auth-cloud-sdk
v0.0.1
Published
Client-side SDK for interacting with authentication and authorization cloud functions.
Maintainers
Readme
Auth Cloud SDK Documentation
Table of Contents
- Introduction
- Getting Started
- Core Concepts
- API Reference
- Type Definitions
- Usage Examples
- Error Handling
- Best Practices
- Troubleshooting
- FAQ
Introduction
The Auth Cloud SDK is a client-side TypeScript/JavaScript library designed to simplify interactions with your custom authentication, authorization, and Firestore-related Google Cloud Functions. It provides a secure and type-safe way to perform authorized Firestore operations, manage authorization relationships, and handle permissions in your web applications.
Key Features
- Authorized Firestore Operations: Execute Firestore operations with built-in authorization checks
- SpiceDB Integration: Manage authorization relationships using SpiceDB
- Type Safety: Full TypeScript support with comprehensive type definitions
- Firebase Authentication: Seamless integration with Firebase Authentication
- Error Handling: Consistent error handling across all operations
Architecture Overview
The Auth Cloud SDK acts as a bridge between your client application and your backend Google Cloud Functions. It leverages:
- Firebase Authentication: For user identity and secure token generation
- SpiceDB: For fine-grained authorization and permission management
- Firestore: For data storage and retrieval
- Google Cloud Functions: For secure server-side operations
┌─────────────────┐ ┌───────────────┐ ┌─────────────────────┐
│ │ │ │ │ │
│ Client App │──────▶ Auth Cloud │──────▶ Google Cloud │
│ (React, Vue, │ │ SDK │ │ Functions │
│ Angular, etc) │◀─────│ │◀─────│ │
│ │ │ │ │ │
└─────────────────┘ └───────────────┘ └─────────────────────┘
│
│
▼
┌─────────────────────┐
│ │
│ Firebase Auth │
│ Firestore │
│ SpiceDB │
│ │
└─────────────────────┘Getting Started
Installation
Install the Auth Cloud SDK using npm or yarn:
npm install auth-cloud-sdk
# or
yarn add auth-cloud-sdkPrerequisites
Before using the Auth Cloud SDK, ensure you have:
- Firebase Initialized: Your client application must initialize Firebase, particularly Firebase Authentication.
// Example Firebase initialization
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);Deployed Cloud Functions: The following Google Cloud Functions must be deployed:
executeAuthorizedFirestoreOperationmanageSpiceDBRelationshipwriteSpiceDBSchemalookupAuthorizedResources
SpiceDB Instance: A running SpiceDB instance configured with your authorization schema.
Configuration
Before using any SDK functions, configure it with your Cloud Function endpoints:
import { setCloudFunctionEndpoints } from 'auth-cloud-sdk';
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'https://your-region-your-project.cloudfunctions.net/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'https://your-region-your-project.cloudfunctions.net/manageSpiceDBRelationship',
writeSpiceDBSchema: 'https://your-region-your-project.cloudfunctions.net/writeSpiceDBSchema',
lookupAuthorizedResources: 'https://your-region-your-project.cloudfunctions.net/lookupAuthorizedResources'
});Quick Start Example
Here's a simple example of reading a document with authorization checks:
import { executeAuthorizedFirestoreOperation, Types } from 'auth-cloud-sdk';
async function getSecureDocument(documentId) {
try {
// Define authorization parameters
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: documentId,
permission: 'read'
};
// Define Firestore operation
const firestoreOperation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'secureDocuments',
documentId: documentId
};
// Execute the operation with authorization check
const document = await executeAuthorizedFirestoreOperation(
authorization,
firestoreOperation
);
return document;
} catch (error) {
console.error('Error fetching document:', error);
throw error;
}
}Core Concepts
Authentication Model
The Auth Cloud SDK relies on Firebase Authentication for user identity. When you call any SDK function, it automatically:
- Retrieves the current authenticated user from Firebase Auth
- Gets a fresh ID token for that user
- Sends this token to the Cloud Function for verification
This ensures that all operations are performed in the context of an authenticated user. If no user is signed in, the SDK will throw an error.
Authorization with SpiceDB
The SDK uses SpiceDB (formerly Authzed) as its authorization system. SpiceDB implements a relationship-based authorization model where:
- Resources are objects that users can access (documents, collections, features)
- Relations define how subjects relate to resources (viewer, editor, owner)
- Subjects are typically users, but can also be groups or other entities
- Permissions are derived from relations through a schema
The SDK provides functions to:
- Create, update, or delete relationships
- Check if a user has permission on a resource
- Find all resources a user has permission to access
- Update the authorization schema
Firestore Operations with Authorization
The SDK combines Firestore operations with authorization checks in a single call. This ensures that users can only perform operations they're authorized for. Supported operations include:
- Reading documents
- Querying collections
- Creating documents
- Updating documents
- Deleting documents
Each operation is checked against the user's permissions before execution.
Relationship Management
The SDK allows you to manage authorization relationships in SpiceDB. This includes:
- Creating new relationships (granting permissions)
- Touching relationships (updating timestamps)
- Deleting relationships (revoking permissions)
API Reference
Configuration Functions
setCloudFunctionEndpoints(endpoints: CloudFunctionEndpoints): void
Configures the SDK with the necessary Cloud Function endpoint URLs. This must be called once before using any SDK functions.
Parameters:
endpoints: An object containing the URLs for the Cloud Functions.executeAuthorizedFirestoreOperation: URL for the Firestore operations functionmanageSpiceDBRelationship: URL for the relationship management functionwriteSpiceDBSchema: URL for the schema writing functionlookupAuthorizedResources: URL for the resource lookup function
Throws:
- Error if endpoints configuration is empty
- Error if any required endpoint is missing
Example:
import { setCloudFunctionEndpoints } from 'auth-cloud-sdk';
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'https://us-central1-myproject.cloudfunctions.net/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'https://us-central1-myproject.cloudfunctions.net/manageSpiceDBRelationship',
writeSpiceDBSchema: 'https://us-central1-myproject.cloudfunctions.net/writeSpiceDBSchema',
lookupAuthorizedResources: 'https://us-central1-myproject.cloudfunctions.net/lookupAuthorizedResources'
});getCloudFunctionEndpoints(): CloudFunctionEndpoints
Retrieves the configured Cloud Function endpoints.
Returns:
- An object containing the configured endpoint URLs
Throws:
- Error if endpoints have not been set
Example:
import { getCloudFunctionEndpoints } from 'auth-cloud-sdk';
try {
const endpoints = getCloudFunctionEndpoints();
console.log('Configured endpoints:', endpoints);
} catch (error) {
console.error('Endpoints not configured:', error);
}Firestore Operations
executeAuthorizedFirestoreOperation(authorization: Authorization, firestoreOperation: FirestoreOperation): Promise<any>
Executes a Firestore operation through the designated Cloud Function, with authorization checks.
Parameters:
authorization: Authorization details for the operationresourceObjectType: Type of resource being accessedresourceObjectId: ID of the resource being accessedpermission: Permission required for the operationsubjectObjectType: (Optional) Type of the subject, defaults to 'user'zedToken: (Optional) ZedToken for consistency
firestoreOperation: The Firestore operation to performoperationType: Type of operation ('GET_DOCUMENT', 'GET_COLLECTION', 'CREATE_DOCUMENT', 'UPDATE_DOCUMENT', 'DELETE_DOCUMENT')collectionPath: Path to the Firestore collectiondocumentId: (Optional) ID of the documentdata: (Optional) Data for create/update operationsqueryConstraints: (Optional) Query constraints for collection queries
Returns:
- A promise that resolves with the result of the Firestore operation
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
- Error if the user lacks the required permission
Example:
import { executeAuthorizedFirestoreOperation, Types } from 'auth-cloud-sdk';
// Read a document
async function readDocument(docId) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'read'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'documents',
documentId: docId
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Query a collection
async function queryDocuments() {
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'list'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_COLLECTION',
collectionPath: 'documents',
queryConstraints: [
{ field: 'status', op: '==', value: 'published' },
{ orderBy: 'createdAt', direction: 'desc' },
{ limit: 10 }
]
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Create a document
async function createDocument(data) {
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'create'
};
const operation: Types.FirestoreOperation = {
operationType: 'CREATE_DOCUMENT',
collectionPath: 'documents',
data: data
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Update a document
async function updateDocument(docId, data) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'update'
};
const operation: Types.FirestoreOperation = {
operationType: 'UPDATE_DOCUMENT',
collectionPath: 'documents',
documentId: docId,
data: data
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}
// Delete a document
async function deleteDocument(docId) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'delete'
};
const operation: Types.FirestoreOperation = {
operationType: 'DELETE_DOCUMENT',
collectionPath: 'documents',
documentId: docId
};
return executeAuthorizedFirestoreOperation(authorization, operation);
}SpiceDB Relationship Management
manageSpiceDBRelationship(payload: ManageRelationshipPayload): Promise<ManageRelationshipResponse>
Manages SpiceDB relationships (create, touch, delete) through the Cloud Function.
Parameters:
payload: The payload containing the operation type and relationshipsoperation: Type of operation ('CREATE', 'TOUCH', 'DELETE')relationships: Array of relationship objectsresource: The resource object (type and ID)relation: The relation namesubject: The subject object (type and ID)
Returns:
- A promise that resolves with the result of the operation
status: Operation status ('success')operation: The operation that was performedzedToken: ZedToken for consistency
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
Example:
import { manageSpiceDBRelationship, Types } from 'auth-cloud-sdk';
// Grant a user viewer access to a document
async function grantViewAccess(userId, documentId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'viewer',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Grant a user editor access to a document
async function grantEditAccess(userId, documentId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'editor',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Revoke a user's access to a document
async function revokeAccess(userId, documentId, relation) {
const payload: Types.ManageRelationshipPayload = {
operation: 'DELETE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: relation, // 'viewer', 'editor', etc.
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}writeSpiceDBSchema(schemaText: string): Promise<WriteSchemaResponse>
Writes a new schema to SpiceDB through the Cloud Function. This operation is typically restricted to super admins.
Parameters:
schemaText: The SpiceDB schema definition text
Returns:
- A promise that resolves with the result of the operation
status: Operation status ('success')message: Success message
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
- Error if the user is not authorized to write schemas
Example:
import { writeSpiceDBSchema } from 'auth-cloud-sdk';
// Update the authorization schema
async function updateAuthSchema() {
const schemaText = `
definition user {}
definition document {
relation viewer: user
relation editor: user
relation owner: user
permission view = viewer + editor + owner
permission edit = editor + owner
permission delete = owner
}
definition collection {
relation admin: user
permission create = admin
permission list = admin
}
`;
return writeSpiceDBSchema(schemaText);
}lookupAuthorizedResources(resourceObjectType: string, permission: string, subjectObjectType?: string, zedToken?: string): Promise<LookupAuthorizedResourcesResponse>
Looks up resources a subject has a given permission on, via the Cloud Function.
Parameters:
resourceObjectType: The type of resource to look up (e.g., "document", "folder")permission: The permission to check (e.g., "view", "edit")subjectObjectType: (Optional) The type of the subject (e.g., "user"). Defaults to "user" if undefinedzedToken: (Optional) ZedToken for consistency
Returns:
- A promise that resolves with a list of resource IDs
resourceIds: Array of resource IDs the user has permission on
Throws:
- Error if the user is not authenticated
- Error if the Cloud Function call fails
Example:
import { lookupAuthorizedResources } from 'auth-cloud-sdk';
// Find all documents the user can view
async function findViewableDocuments() {
const result = await lookupAuthorizedResources('document', 'view');
return result.resourceIds;
}
// Find all documents the user can edit
async function findEditableDocuments() {
const result = await lookupAuthorizedResources('document', 'edit');
return result.resourceIds;
}
// Find all collections the user can create documents in
async function findWritableCollections() {
const result = await lookupAuthorizedResources('collection', 'create');
return result.resourceIds;
}Type Definitions
The SDK exports the following TypeScript interfaces:
CloudFunctionEndpoints
Defines the structure for the Cloud Function endpoints configuration.
interface CloudFunctionEndpoints {
executeAuthorizedFirestoreOperation: string;
manageSpiceDBRelationship: string;
writeSpiceDBSchema: string;
lookupAuthorizedResources: string;
}FirestoreOperation
Defines the structure for a Firestore operation.
interface FirestoreOperation {
operationType: 'GET_DOCUMENT' | 'GET_COLLECTION' | 'CREATE_DOCUMENT' | 'UPDATE_DOCUMENT' | 'DELETE_DOCUMENT';
collectionPath: string;
documentId?: string;
data?: any;
queryConstraints?: Array<{
field?: string;
op?: WhereFilterOp;
value?: any;
orderBy?: string;
direction?: 'asc' | 'desc';
limit?: number;
}>;
}Authorization
Defines the authorization details required for an operation.
interface Authorization {
subjectObjectType?: string; // Defaults to 'user' in the Cloud Function if not provided
resourceObjectType: string;
resourceObjectId: string;
permission: string;
zedToken?: string; // Optional ZedToken for consistency
}Relationship
Defines the structure for a single relationship to be managed in SpiceDB.
interface Relationship {
resource: {
objectType: string;
objectId: string;
};
relation: string;
subject: {
object: {
objectType?: string; // Defaults to 'user' in the Cloud Function if not provided
objectId: string;
};
optionalRelation?: string;
};
}ManageRelationshipPayload
Defines the payload for managing SpiceDB relationships.
interface ManageRelationshipPayload {
operation: 'CREATE' | 'TOUCH' | 'DELETE';
relationships: Relationship[];
}ManageRelationshipResponse
Defines the response structure for a successful relationship management operation.
interface ManageRelationshipResponse {
status: 'success';
operation: 'CREATE' | 'TOUCH' | 'DELETE';
zedToken: string;
}WriteSchemaResponse
Defines the response structure for a successful schema write operation.
interface WriteSchemaResponse {
status: 'success';
message: string;
}LookupAuthorizedResourcesResponse
Defines the response structure for looking up authorized resources.
interface LookupAuthorizedResourcesResponse {
resourceIds: string[];
}CloudFunctionErrorResponse
Generic error response structure from Cloud Functions.
interface CloudFunctionErrorResponse {
error: string;
}Usage Examples
Building a Secure Document Management System
import {
executeAuthorizedFirestoreOperation,
manageSpiceDBRelationship,
lookupAuthorizedResources,
Types
} from 'auth-cloud-sdk';
class SecureDocumentManager {
// Create a new document with the current user as owner
async createDocument(title, content) {
const currentUser = firebase.auth().currentUser;
if (!currentUser) throw new Error('User not authenticated');
// First, create the document
const authorization: Types.Authorization = {
resourceObjectType: 'collection',
resourceObjectId: 'documents',
permission: 'create'
};
const createOperation: Types.FirestoreOperation = {
operationType: 'CREATE_DOCUMENT',
collectionPath: 'documents',
data: {
title,
content,
createdBy: currentUser.uid,
createdAt: new Date().toISOString()
}
};
const newDoc = await executeAuthorizedFirestoreOperation(authorization, createOperation);
// Then, set the current user as the owner
const relationshipPayload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: newDoc.id
},
relation: 'owner',
subject: {
object: {
objectType: 'user',
objectId: currentUser.uid
}
}
}
]
};
await manageSpiceDBRelationship(relationshipPayload);
return newDoc;
}
// Get all documents the user can view
async getViewableDocuments() {
// First, get all document IDs the user can view
const lookupResult = await lookupAuthorizedResources('document', 'view');
const documentIds = lookupResult.resourceIds;
if (documentIds.length === 0) {
return [];
}
// Then, fetch the documents in batches
const documents = [];
const batchSize = 10;
for (let i = 0; i < documentIds.length; i += batchSize) {
const batch = documentIds.slice(i, i + batchSize);
for (const docId of batch) {
const authorization: Types.Authorization = {
resourceObjectType: 'document',
resourceObjectId: docId,
permission: 'view'
};
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT',
collectionPath: 'documents',
documentId: docId
};
try {
const doc = await executeAuthorizedFirestoreOperation(authorization, operation);
documents.push({ id: docId, ...doc });
} catch (error) {
console.error(`Error fetching document ${docId}:`, error);
// Continue with other documents
}
}
}
return documents;
}
// Share a document with another user
async shareDocument(documentId, userId, permission) {
// Map permission to relation
const relationMap = {
'view': 'viewer',
'edit': 'editor'
};
const relation = relationMap[permission];
if (!relation) {
throw new Error(`Invalid permission: ${permission}`);
}
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: relation,
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Revoke access to a document
async revokeAccess(documentId, userId, permission) {
// Map permission to relation
const relationMap = {
'view': 'viewer',
'edit': 'editor'
};
const relation = relationMap[permission];
if (!relation) {
throw new Error(`Invalid permission: ${permission}`);
}
const payload: Types.ManageRelationshipPayload = {
operation: 'DELETE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: relation,
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
}Implementing Team-Based Access Control
import {
manageSpiceDBRelationship,
Types
} from 'auth-cloud-sdk';
class TeamAccessManager {
// Add a user to a team
async addUserToTeam(userId, teamId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'team',
objectId: teamId
},
relation: 'member',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Grant a team access to a resource
async grantTeamAccess(teamId, resourceType, resourceId, permission) {
// Map permission to relation
const relationMap = {
'view': 'viewer',
'edit': 'editor'
};
const relation = relationMap[permission];
if (!relation) {
throw new Error(`Invalid permission: ${permission}`);
}
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: resourceType,
objectId: resourceId
},
relation: relation,
subject: {
object: {
objectType: 'team',
objectId: teamId
},
optionalRelation: 'member'
}
}
]
};
return manageSpiceDBRelationship(payload);
}
// Remove a user from a team
async removeUserFromTeam(userId, teamId) {
const payload: Types.ManageRelationshipPayload = {
operation: 'DELETE',
relationships: [
{
resource: {
objectType: 'team',
objectId: teamId
},
relation: 'member',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
return manageSpiceDBRelationship(payload);
}
}Error Handling
The Auth Cloud SDK provides consistent error handling across all functions. Here are common errors and how to handle them:
Authentication Errors
These occur when there's no authenticated user or when token retrieval fails.
import { executeAuthorizedFirestoreOperation } from 'auth-cloud-sdk';
try {
const result = await executeAuthorizedFirestoreOperation(/* ... */);
// Process result
} catch (error) {
if (error.message.includes('No authenticated user found')) {
// Handle unauthenticated user
console.error('User is not signed in');
// Redirect to login page
} else if (error.message.includes('Failed to retrieve Firebase ID token')) {
// Handle token retrieval failure
console.error('Authentication token error:', error);
// Prompt user to sign in again
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}Authorization Errors
These occur when a user attempts an operation they're not authorized for.
try {
const result = await executeAuthorizedFirestoreOperation(/* ... */);
// Process result
} catch (error) {
if (error.message.includes('Permission denied') ||
error.message.includes('not authorized')) {
// Handle permission denied
console.error('User does not have permission for this operation');
// Show appropriate UI message
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}Network Errors
These occur when the Cloud Function cannot be reached.
try {
const result = await executeAuthorizedFirestoreOperation(/* ... */);
// Process result
} catch (error) {
if (error.message.includes('Failed to fetch') ||
error.message.includes('Network error')) {
// Handle network issues
console.error('Network error:', error);
// Show offline message or retry option
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}Cloud Function Errors
These are errors returned by the Cloud Functions themselves.
try {
const result = await executeAuthorizedFirestoreOperation(/* ... */);
// Process result
} catch (error) {
if (error.message.includes('Cloud Function Error')) {
// Extract status code if available
const statusMatch = error.message.match(/status (\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1]) : null;
if (statusCode === 400) {
// Handle bad request
console.error('Invalid request:', error);
} else if (statusCode === 403) {
// Handle forbidden
console.error('Permission denied:', error);
} else if (statusCode === 404) {
// Handle not found
console.error('Resource not found:', error);
} else if (statusCode === 500) {
// Handle server error
console.error('Server error:', error);
} else {
// Handle other Cloud Function errors
console.error('Cloud Function error:', error);
}
} else {
// Handle other errors
console.error('Operation failed:', error);
}
}Best Practices
Security Considerations
Always verify authentication state before making SDK calls:
if (!firebase.auth().currentUser) { // Redirect to login or show authentication required message return; }Use the principle of least privilege when defining permissions:
// Instead of granting editor access when viewer would suffice: const relation = userNeedsToEdit ? 'editor' : 'viewer';Validate inputs before passing them to SDK functions:
function validateDocumentId(id) { if (!id || typeof id !== 'string' || id.trim() === '') { throw new Error('Invalid document ID'); } return id; }Don't store sensitive information in client-accessible Firestore documents:
// Bad practice const userData = { name: 'User', password: 'hashed_password', // Never store this client-side! apiKeys: ['secret_key_1', 'secret_key_2'] // Never store this client-side! }; // Good practice const userData = { name: 'User', settings: { theme: 'dark', notifications: true } };Implement proper error handling for all SDK calls:
try { await executeAuthorizedFirestoreOperation(/* ... */); } catch (error) { // Log the error console.error('Operation failed:', error); // Show appropriate user message if (error.message.includes('Permission denied')) { showUserMessage('You don\'t have permission to perform this action'); } else { showUserMessage('An error occurred. Please try again later.'); } // Report to monitoring system if needed reportErrorToMonitoring(error); }
Performance Optimization
Batch related operations when possible:
// Instead of multiple separate relationship creations: const payload: Types.ManageRelationshipPayload = { operation: 'CREATE', relationships: [ { /* relationship 1 */ }, { /* relationship 2 */ }, { /* relationship 3 */ } ] }; await manageSpiceDBRelationship(payload);Cache authorization results when appropriate:
// Cache the list of viewable documents for a short time let viewableDocumentsCache = { timestamp: 0, data: [] }; async function getViewableDocuments() { const now = Date.now(); const cacheLifetime = 60 * 1000; // 1 minute if (now - viewableDocumentsCache.timestamp < cacheLifetime) { return viewableDocumentsCache.data; } const result = await lookupAuthorizedResources('document', 'view'); viewableDocumentsCache = { timestamp: now, data: result.resourceIds }; return result.resourceIds; }Use query constraints to limit data transfer:
const operation: Types.FirestoreOperation = { operationType: 'GET_COLLECTION', collectionPath: 'documents', queryConstraints: [ { limit: 10 }, // Only get 10 documents { orderBy: 'updatedAt', direction: 'desc' } // Get most recent first ] };Implement pagination for large result sets:
async function getDocumentPage(lastDocId, pageSize = 10) { const authorization: Types.Authorization = { resourceObjectType: 'collection', resourceObjectId: 'documents', permission: 'list' }; const operation: Types.FirestoreOperation = { operationType: 'GET_COLLECTION', collectionPath: 'documents', queryConstraints: [ { orderBy: 'createdAt', direction: 'desc' }, { limit: pageSize } ] }; // Add startAfter constraint if we have a last document ID if (lastDocId) { // First get the last document const lastDocOp: Types.FirestoreOperation = { operationType: 'GET_DOCUMENT', collectionPath: 'documents', documentId: lastDocId }; const lastDoc = await executeAuthorizedFirestoreOperation(authorization, lastDocOp); // Then add startAfter to the query constraints operation.queryConstraints.push({ startAfter: lastDoc.createdAt }); } return executeAuthorizedFirestoreOperation(authorization, operation); }
Integration Patterns
Implement a service layer to abstract SDK calls:
// documents.service.ts import { executeAuthorizedFirestoreOperation, Types } from 'auth-cloud-sdk'; export class DocumentService { async getDocument(id) { const authorization: Types.Authorization = { resourceObjectType: 'document', resourceObjectId: id, permission: 'read' }; const operation: Types.FirestoreOperation = { operationType: 'GET_DOCUMENT', collectionPath: 'documents', documentId: id }; return executeAuthorizedFirestoreOperation(authorization, operation); } // Other document-related methods... }Use dependency injection for testability:
// In a React component with dependency injection function DocumentViewer({ documentId, documentService }) { const [document, setDocument] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadDocument() { try { const doc = await documentService.getDocument(documentId); setDocument(doc); } catch (err) { setError(err); } finally { setLoading(false); } } loadDocument(); }, [documentId, documentService]); // Render component... }Implement retry logic for transient errors:
async function executeWithRetry(fn, maxRetries = 3, delay = 1000) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Only retry for certain errors if (!isRetryableError(error) || attempt === maxRetries) { throw error; } // Wait before retrying await new Promise(resolve => setTimeout(resolve, delay * attempt)); } } throw lastError; } function isRetryableError(error) { // Determine if this error should be retried return error.message.includes('network error') || error.message.includes('timeout') || error.message.includes('ECONNRESET'); } // Usage const result = await executeWithRetry(() => executeAuthorizedFirestoreOperation(authorization, operation) );
Troubleshooting
Common Issues and Solutions
"Cloud Function endpoints not configured"
Problem: You're seeing an error about endpoints not being configured.
Solution: Ensure you've called setCloudFunctionEndpoints before using any SDK functions:
import { setCloudFunctionEndpoints } from 'auth-cloud-sdk';
// Call this early in your application initialization
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'https://your-region-your-project.cloudfunctions.net/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'https://your-region-your-project.cloudfunctions.net/manageSpiceDBRelationship',
writeSpiceDBSchema: 'https://your-region-your-project.cloudfunctions.net/writeSpiceDBSchema',
lookupAuthorizedResources: 'https://your-region-your-project.cloudfunctions.net/lookupAuthorizedResources'
});"No authenticated user found"
Problem: You're trying to use SDK functions without a signed-in user.
Solution: Ensure the user is authenticated before making SDK calls:
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
// Sign in the user first
const auth = getAuth();
await signInWithEmailAndPassword(auth, email, password);
// Now you can use SDK functions
const result = await executeAuthorizedFirestoreOperation(/* ... */);"Permission denied" or "not authorized"
Problem: The user doesn't have the required permissions for the operation.
Solution: Check and update the user's permissions:
import { manageSpiceDBRelationship, Types } from 'auth-cloud-sdk';
// Grant the necessary permission
const payload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'viewer', // or 'editor', 'owner', etc.
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
await manageSpiceDBRelationship(payload);"Failed to fetch" or Network Errors
Problem: The SDK can't reach the Cloud Functions.
Solution: Check your network connection and Cloud Function URLs:
- Verify the Cloud Function URLs are correct
- Ensure the Cloud Functions are deployed and running
- Check for CORS issues if calling from a browser
- Verify the Cloud Functions are in the same region as your application for best performance
"Invalid request" (400 errors)
Problem: The request to the Cloud Function is malformed.
Solution: Check your parameters and ensure they match the expected format:
// Correct format for authorization
const authorization: Types.Authorization = {
resourceObjectType: 'document', // Required
resourceObjectId: documentId, // Required
permission: 'read' // Required
};
// Correct format for Firestore operation
const operation: Types.FirestoreOperation = {
operationType: 'GET_DOCUMENT', // Required
collectionPath: 'documents', // Required
documentId: documentId // Required for GET_DOCUMENT
};Debugging Tips
Enable verbose logging:
// Add this early in your application const DEBUG = true; function debugLog(...args) { if (DEBUG) { console.log('[Auth Cloud SDK]', ...args); } } // Use throughout your code debugLog('Calling executeAuthorizedFirestoreOperation with:', authorization, operation);Check Firebase Authentication state:
import { getAuth, onAuthStateChanged } from 'firebase/auth'; const auth = getAuth(); onAuthStateChanged(auth, (user) => { if (user) { console.log('User is signed in:', user.uid); user.getIdTokenResult(true).then(idTokenResult => { console.log('User claims:', idTokenResult.claims); }); } else { console.log('No user is signed in'); } });Verify Cloud Function endpoints:
import { getCloudFunctionEndpoints } from 'auth-cloud-sdk'; try { const endpoints = getCloudFunctionEndpoints(); console.log('Configured endpoints:', endpoints); } catch (error) { console.error('Endpoints not configured:', error); }Test Cloud Functions directly:
// Test a Cloud Function directly with fetch async function testCloudFunction() { const auth = getAuth(); const user = auth.currentUser; if (!user) { console.error('No user signed in'); return; } const idToken = await user.getIdToken(); const endpoint = 'https://your-region-your-project.cloudfunctions.net/lookupAuthorizedResources'; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ firebaseToken: idToken, resourceObjectType: 'document', permission: 'view' }) }); if (!response.ok) { const errorText = await response.text(); console.error(`Error ${response.status}:`, errorText); } else { const result = await response.json(); console.log('Success:', result); } } catch (error) { console.error('Fetch error:', error); } }
FAQ
General Questions
Q: Do I need to initialize Firebase before using the SDK?
A: Yes, you must initialize Firebase in your application before using the SDK. The SDK relies on the Firebase Auth instance to get the current user and their ID token.
Q: Can I use the SDK with any Firebase project?
A: Yes, but you need to deploy the corresponding Cloud Functions to your Firebase project. The SDK is designed to work with specific Cloud Functions that handle authorization and Firestore operations.
Q: Does the SDK work with Firebase Emulator Suite?
A: Yes, you can use the SDK with Firebase Emulator Suite for local development. Just configure the SDK with the local URLs of your emulated Cloud Functions.
setCloudFunctionEndpoints({
executeAuthorizedFirestoreOperation: 'http://localhost:5001/your-project/us-central1/executeAuthorizedFirestoreOperation',
manageSpiceDBRelationship: 'http://localhost:5001/your-project/us-central1/manageSpiceDBRelationship',
writeSpiceDBSchema: 'http://localhost:5001/your-project/us-central1/writeSpiceDBSchema',
lookupAuthorizedResources: 'http://localhost:5001/your-project/us-central1/lookupAuthorizedResources'
});Authentication Questions
Q: How does the SDK handle authentication?
A: The SDK uses Firebase Authentication. It retrieves the current user's ID token and sends it to the Cloud Functions for verification. The Cloud Functions then use this token to authenticate the user and perform the requested operations.
Q: What happens if the user's token expires?
A: The SDK automatically requests a fresh token when needed. If the token has expired, the getFirebaseIdToken function will force a refresh.
Q: Can I use custom authentication with the SDK?
A: The SDK is designed to work with Firebase Authentication. If you're using custom authentication, you'll need to integrate it with Firebase Auth using Custom Auth Providers.
Authorization Questions
Q: What is SpiceDB and why is it used?
A: SpiceDB (formerly Authzed) is an open-source, fine-grained permissions system. It's used to manage authorization relationships and check permissions. The SDK uses SpiceDB through Cloud Functions to provide robust authorization capabilities.
Q: How do I define permissions in SpiceDB?
A: Permissions in SpiceDB are defined through a schema. You can write and update this schema using the writeSpiceDBSchema function. Here's an example schema:
definition user {}
definition document {
relation viewer: user
relation editor: user
relation owner: user
permission view = viewer + editor + owner
permission edit = editor + owner
permission delete = owner
}Q: Can I use role-based access control (RBAC) with the SDK?
A: Yes, you can implement RBAC using SpiceDB relationships. For example:
// Define roles in your SpiceDB schema
const schema = `
definition user {}
definition role {
relation member: user
}
definition document {
relation viewer: user | role#member
relation editor: user | role#member
permission view = viewer
permission edit = editor
}
`;
// Assign a user to a role
const assignRolePayload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'role',
objectId: 'admin'
},
relation: 'member',
subject: {
object: {
objectType: 'user',
objectId: userId
}
}
}
]
};
// Grant a role access to a document
const grantRoleAccessPayload: Types.ManageRelationshipPayload = {
operation: 'CREATE',
relationships: [
{
resource: {
objectType: 'document',
objectId: documentId
},
relation: 'editor',
subject: {
object: {
objectType: 'role',
objectId: 'admin'
}
}
}
]
};Firestore Questions
Q: Does the SDK replace the Firestore SDK?
A: No, the SDK complements the Firestore SDK by adding authorization checks. It's designed for operations that require permission checks, but you can still use the regular Firestore SDK for operations that don't need authorization.
Q: Can I use the SDK with Firestore Security Rules?
A: Yes, but they serve different purposes. Firestore Security Rules provide client-side security, while the SDK provides server-side security through Cloud Functions. You can use both together for enhanced security.
Q: How does the SDK handle Firestore transactions?
A: The SDK doesn't directly support Firestore transactions. For operations that require transactions, you should create a dedicated Cloud Function that performs the transaction and call it through the SDK.
Development and Deployment Questions
Q: How do I update the SDK to a new version?
A: Update the SDK using npm or yarn:
npm update auth-cloud-sdk
# or
yarn upgrade auth-cloud-sdkQ: Can I contribute to the SDK?
A: Yes, contributions are welcome. Check the repository for contribution guidelines.
Q: How do I report bugs or request features?
A: Report bugs and request features through the repository's issue tracker.
Now that you have a comprehensive understanding of the Auth Cloud SDK, you can build secure, authorized applications with confidence. The SDK provides a robust foundation for implementing complex authorization patterns while keeping your code clean and maintainable.
