Introduction to Security & Authentication in SpaceTimeDB
Welcome to Chapter 12! As we venture further into building sophisticated real-time applications with SpaceTimeDB, securing our data and controlling access becomes paramount. Just as you wouldn’t leave your front door unlocked, we can’t deploy an application without robust authentication and authorization mechanisms. This chapter will equip you with the knowledge and practical skills to safeguard your SpaceTimeDB applications.
In this chapter, we’ll unravel SpaceTimeDB’s unique approach to security, which tightly integrates authentication and authorization directly into your backend logic (reducers). We’ll explore how to identify users, manage their identities, and critically, how to enforce granular permissions for every action and data access within your SpaceTimeDB instance. By the end, you’ll be able to design and implement secure, multi-user real-time systems with confidence.
Before diving in, we assume you’re comfortable with SpaceTimeDB’s core concepts, including defining schemas, writing reducers, and understanding the client-server interaction, as covered in previous chapters. Let’s make our real-time applications not just fast and collaborative, but secure!
Core Concepts: Protecting Your Real-Time World
Securing real-time, collaborative applications presents unique challenges. Unlike traditional request-response systems where a single HTTP request might carry all necessary authentication headers, real-time systems maintain persistent connections, and state changes can originate from multiple clients simultaneously. SpaceTimeDB addresses this by embedding security directly into its deterministic, event-driven model.
The Security Landscape: Authentication vs. Authorization
Let’s start by clarifying two fundamental terms that are often used interchangeably but have distinct meanings:
- Authentication: This is the process of verifying who a user is. When you log in with a username and password, or use a social login (like Google or GitHub), you are authenticating. The system confirms your identity.
- Authorization: This is the process of determining what an authenticated user is allowed to do. Once the system knows who you are, it decides if you have permission to read a specific piece of data, write to a particular table, or execute a certain action.
SpaceTimeDB handles both, but with a clear separation of concerns. You’ll typically integrate with an external authentication provider and then use SpaceTimeDB’s built-in mechanisms for authorization.
SpaceTimeDB’s Security Model Overview
SpaceTimeDB’s security model centers around a few key ideas:
- The
CallerObject: Every reducer call in SpaceTimeDB has access to acallerobject. This object holds information about the client that initiated the reducer call, most importantly, theiridentity_id. - The
authReducer: This special reducer is responsible for authenticating clients. When a client connects to SpaceTimeDB and wants to be identified, it calls theauthreducer with some form of credentials (e.g., a JWT, an API key). Theauthreducer validates these credentials and returns anIdentityId. - The
identityTable: This is a special, internal SpaceTimeDB table that stores unique identifiers for authenticated clients. Theauthreducer manages entries in this table. - Reducer-based Authorization: Once a client is authenticated and has an
identity_idassociated with their connection (via thecallerobject), all subsequent data-modifying reducers can inspectcaller.identity_idto enforce authorization rules.
Let’s visualize this flow:
Explanation of the Flow:
- Client Authenticates Externally: Your frontend application first authenticates the user with an external identity provider (like Auth0, Firebase Auth, Google, etc.). This is crucial because SpaceTimeDB itself is not an identity provider.
- Token Received: The external service returns a secure token (commonly a JSON Web Token or JWT) to the client.
- Client Connects to SpaceTimeDB: The client connects to your SpaceTimeDB instance, typically passing this token as part of the connection or immediately calling the
authreducer. authReducer Invocation: The client explicitly calls your customauthreducer, passing the token.- Token Validation & Identity Management: Inside the
authreducer, you validate the token (e.g., verify its signature, expiration, issuer). If valid, you either retrieve an existing identity from theidentitytable or create a new one. - IdentityId Returned: The
authreducer returns theIdentityIdassociated with the authenticated user. - Server Associates Identity: SpaceTimeDB internally associates this
IdentityIdwith the client’s connection. callerObject Populated: For all subsequent reducer calls made by this client, thecallerobject will containcaller.identity_id, allowing your reducers to know who is making the request.- Authorization in Data Reducers: Your application’s data reducers (e.g.,
create_post,update_profile) can then checkcaller.identity_idto enforce specific authorization rules before modifying or querying data inDBStorage.
The grant_access Statement
While most authorization is handled by logic within your reducers, SpaceTimeDB also provides a grant_access statement. This is a powerful feature for defining table-level or row-level read permissions directly in your schema. It specifies which IdentityIds (or roles/groups derived from identities) are allowed to subscribe to or query specific data. This is particularly useful for preventing unauthorized clients from even seeing data they shouldn’t have access to, reducing network traffic and simplifying reducer logic.
For example, you might grant_access to a private_messages table only to the identities that are participants in the conversation, or to an admin_dashboard table only to identities with an “admin” role. We’ll focus on reducer-based authorization first for actions, and then touch upon grant_access for read permissions.
Step-by-Step Implementation: Building a Simple Authenticated System
Let’s put these concepts into practice. We’ll build a simplified authentication and authorization system. For the sake of this tutorial, we’ll mock external token validation. In a real application, you would integrate with an external library or service to verify JWTs.
First, let’s ensure you have SpaceTimeDB CLI v2.x installed. If not, refer to Chapter 2 for installation instructions.
1. Initialize Your Project
If you don’t have a project already, let’s create a new one:
# Ensure you have the SpaceTimeDB CLI (v2.x)
# Install via: curl -sSL https://get.spacetimedb.com | bash
# Verify: spacetime --version
# Create a new project directory
mkdir my_secure_app
cd my_secure_app
# Initialize SpaceTimeDB project
spacetime new --name my_secure_app
2. Define the User Table
While SpaceTimeDB automatically manages the identity table, we’ll often want to store additional user-specific data (e.g., username, email, profile picture). Let’s define a User table that links directly to an IdentityId.
Open src/lib.spacetimedb/schema.spacetimedb and add the following:
// src/lib.spacetimedb/schema.spacetimedb
// The Identity table is automatically managed by SpaceTimeDB.
// We'll define a User table that links to an Identity.
// @spacetimedb_id: This attribute marks 'identity_id' as the primary key
// and also indicates it's linked to an Identity.
// It ensures a one-to-one relationship between a User and an Identity.
table User {
#[spacetimedb(primarykey)]
#[spacetimedb(id)]
identity_id: IdentityId, // Links directly to a SpaceTimeDB Identity
username: String,
email: String,
created_at: u64,
}
// A simple Post table that will be associated with a User/Identity
table Post {
#[spacetimedb(primarykey)]
post_id: u64,
author_identity_id: IdentityId, // Who wrote the post
content: String,
created_at: u64,
}
After modifying the schema, always run:
spacetime generate
This command updates the generated Rust code for your reducers and types.
3. Implement the auth Reducer
Now, let’s create the auth reducer. This reducer will take a “token” (a simple string in our mocked example), validate it, and return an IdentityId.
Open src/lib.spacetimedb/mod.rs and add the following reducer logic. Remember, in a real application, validate_token_and_extract_user_info would involve cryptographic verification of a JWT with a public key from your external auth provider.
// src/lib.spacetimedb/mod.rs
use spacetimedb::{
spacetimedb,
Identity, IdentityId,
// Add other necessary imports, e.g., for `User` if you're using it
// use crate::User; // Assuming User is in the same module or imported
};
// Import the generated schema types
use crate::{
User, Post,
// Add other generated types as needed
};
// --- Helper for Mock Token Validation (DO NOT USE IN PRODUCTION) ---
// In a real application, this would involve verifying a JWT signature,
// checking claims, expiration, etc., using a robust JWT library.
fn validate_token_and_extract_user_info(token: String) -> Option<(String, String)> {
// For this example, let's mock a simple token validation.
// A real token would be a complex JWT.
if token.starts_with("mock_token_") {
let parts: Vec<&str> = token.split('_').collect();
if parts.len() == 3 {
let username = parts[1].to_string();
let email = format!("{}@example.com", parts[1]);
return Some((username, email));
}
}
None
}
// --- End Helper ---
/// The `auth` reducer is special. It's called by clients to authenticate themselves.
/// It receives a `token` (e.g., a JWT) and, if valid, returns an `IdentityId`.
/// SpaceTimeDB then associates this `IdentityId` with the client's connection.
#[spacetimedb(reducer)]
pub fn auth(token: String) -> IdentityId {
// 1. Validate the token and extract user information.
// In a real app, you'd use a JWT library (e.g., `jsonwebtoken` crate in Rust)
// to verify the token's signature, expiry, and claims.
let user_info_option = validate_token_and_extract_user_info(token.clone());
let (username, email) = match user_info_option {
Some(info) => info,
None => {
// If the token is invalid, we can either:
// a) Return a default, unauthenticated IdentityId (IdentityId::default() or 0)
// b) Panic, which will disconnect the client (more secure for critical apps)
// For now, let's panic for clear error handling during development.
panic!("Invalid or malformed authentication token: {}", token);
}
};
// 2. Get or create an Identity.
// SpaceTimeDB automatically creates an `Identity` entry if one doesn't exist
// for the given token/user info. The `Identity` table is managed internally.
// We typically use a unique identifier from the external auth provider
// to map to a SpaceTimeDB Identity. For our mock, we'll use the username.
let identity_id = Identity::filter_by_identity_id(IdentityId::hash_from_bytes(username.as_bytes()))
.map(|i| i.identity_id)
.unwrap_or_else(|| {
// If no existing identity, create a new one.
// SpaceTimeDB's `Identity` table is special and often managed implicitly
// or through specific API calls. For direct interaction, we often
// derive a stable IdentityId from a unique external identifier.
// Here, we're using a hash of the username as a stable ID.
// In a real system, you'd use the user ID from your external auth provider.
let new_identity_id = IdentityId::hash_from_bytes(username.as_bytes());
// Note: We don't explicitly `insert` into `Identity` here.
// SpaceTimeDB handles the creation of the `Identity` entry when an `IdentityId`
// is returned by the `auth` reducer or used with `grant_access`.
new_identity_id
});
// 3. Create or update the `User` record associated with this Identity.
// We use `IdentityId::hash_from_bytes(username.as_bytes())` to ensure a consistent ID.
if !User::filter_by_identity_id(identity_id).exists() {
// If the user doesn't exist, create a new User record.
User::insert(User {
identity_id,
username: username.clone(),
email: email.clone(),
created_at: spacetimedb::timestamp(),
}).unwrap(); // Handle potential errors in a production app
} else {
// Optionally update existing user data, e.g., if email changes in external system.
// For simplicity, we'll just ensure it exists.
// let mut user = User::filter_by_identity_id(identity_id).unwrap();
// user.username = username;
// user.email = email;
// user.update().unwrap();
}
// 4. Return the IdentityId. This is crucial!
// SpaceTimeDB will associate this IdentityId with the client's connection.
identity_id
}
Let’s break down the auth reducer:
validate_token_and_extract_user_info: This is a helper function that simulates validating an external authentication token. In a production environment, this would involve much more robust logic, typically using a JWT library to verify the token’s signature against a public key, checking its expiration, and extracting claims likesub(subject/user ID) andemail.IdentityId::hash_from_bytes(username.as_bytes()): We’re deriving a stableIdentityIdfrom the username. In a real system, you’d use the immutable user ID provided by your external authentication service (e.g.,subclaim from a JWT) to ensure consistency. This ID is how SpaceTimeDB internally tracks the authenticated client.User::insertorUser::filter_by_identity_id: After successfully authenticating, we either create a newUserentry in our customUsertable or ensure an existing one is up-to-date. This links our application-specific user data to the SpaceTimeDBIdentityId.return identity_id: The critical step! By returning anIdentityId, SpaceTimeDB knows who this client is for the duration of their connection. All subsequent reducer calls from this client will have access to thisidentity_idvia thecallerobject.
4. Implement an Authenticated Reducer (create_post)
Now that we have an auth reducer, let’s create a reducer that requires authentication and uses the caller object for authorization. We’ll make a create_post reducer that only allows authenticated users to create posts, and automatically associates the post with the author’s identity.
Add this to src/lib.spacetimedb/mod.rs:
// src/lib.spacetimedb/mod.rs (continued)
// Ensure `spacetimedb::timestamp()` is available for `created_at`
use spacetimedb::timestamp;
/// Reducer to create a new post, requiring an authenticated caller.
#[spacetimedb(reducer)]
pub fn create_post(caller: Identity, content: String) {
// Authorization step: Ensure the caller is authenticated.
// If caller.identity_id is IdentityId::default() or 0, it means the client
// has not successfully authenticated via the `auth` reducer.
if caller.identity_id == IdentityId::default() {
panic!("Unauthorized: You must be logged in to create a post.");
}
// Optional: Check if the identity has a corresponding User entry
// This adds another layer of validation, ensuring a 'complete' user.
if !User::filter_by_identity_id(caller.identity_id).exists() {
panic!("Unauthorized: No user profile found for this identity.");
}
// Generate a unique post_id. In a real app, you might use a UUID or
// a more robust sequence generator. For simplicity, we'll hash the content
// and timestamp.
let post_id = spacetimedb::hash_bytes(&format!("{}-{}-{}", caller.identity_id, content, timestamp()).as_bytes());
// Insert the new post, associating it with the author's identity.
Post::insert(Post {
post_id,
author_identity_id: caller.identity_id,
content,
created_at: timestamp(),
}).unwrap();
spacetimedb::log!("Post created successfully by identity: {:?}", caller.identity_id);
}
Key Points in create_post:
caller: Identity: This is how SpaceTimeDB injects thecallerobject into your reducer. It contains theidentity_idof the client making the call.if caller.identity_id == IdentityId::default(): This is your authorization check. Ifcaller.identity_idis the default (unauthenticated) ID, wepanic!, preventing the action. This is a simple but effective authorization gate.author_identity_id: caller.identity_id: We automatically associate the post with the authenticated user’s identity, preventing spoofing.
5. Start SpaceTimeDB and Test
Now, let’s run SpaceTimeDB and test our authentication and authorization.
spacetime dev
This will start your SpaceTimeDB instance. You can interact with it using the CLI or a client SDK.
Testing with spacetime call (CLI):
Try to create a post unauthenticated:
spacetime call create_post 'Hello, unauthenticated world!'You should see an error message similar to:
Reducer call failed: "Unauthorized: You must be logged in to create a post."This confirms our authorization check works!Authenticate as a user:
spacetime call auth 'mock_token_alice'This should succeed and return an
IdentityId. SpaceTimeDB’s CLI maintains the authenticated session for subsequent calls within the same CLI instance.Try to create a post authenticated as Alice:
spacetime call create_post 'Hello from Alice!'This should succeed! You’ll see a log message in your
spacetime devconsole.Authenticate as a different user:
spacetime call auth 'mock_token_bob'Now the CLI is authenticated as Bob.
Create a post as Bob:
spacetime call create_post 'Bob was here.'This will also succeed, and the post will be associated with Bob’s identity.
You can inspect the User and Post tables in the SpaceTimeDB admin UI (usually http://localhost:3000) to see the created entries.
Mini-Challenge: Update Your Own Profile
Now it’s your turn to apply what you’ve learned!
Challenge: Create a new reducer called update_profile that allows an authenticated user to update only their own username and email in the User table.
Hints:
- The reducer should take
caller: Identity, anew_username: String, andnew_email: Stringas arguments. - Perform an authorization check at the beginning, similar to
create_post. - Use
User::filter_by_identity_id(caller.identity_id).unwrap()to find the current user’s profile. - Ensure you are only updating the
Userentry whoseidentity_idmatchescaller.identity_id. If you tried to update another user’s profile, it should panic or return an error.
What to Observe/Learn: This challenge reinforces how to use caller.identity_id for row-level authorization, ensuring users can only modify data they own or are authorized to change.
Common Pitfalls & Troubleshooting
- Forgetting to check
caller.identity_id: The most common mistake! If you don’t explicitly checkcaller.identity_idin your data-modifying reducers, any client (even unauthenticated ones) could potentially call them and modify your data. Always assume reducers are publicly callable unless secured. - Incorrect Token Validation: In a real application, if your
authreducer’s token validation logic is flawed (e.g., not verifying signatures, not checking expiration, or accepting invalid issuers), you risk authenticating malicious clients. Use battle-tested libraries for JWT validation. - Confusing
IdentityId::default()with a valid identity:IdentityId::default()(which is0) represents an unauthenticated caller. Never treat it as a valid, authenticated user ID. - Overly Complex Authorization Logic: While reducers are powerful, try to keep authorization logic concise and clear. For very complex role-based access control (RBAC) or attribute-based access control (ABAC), you might pre-process roles/permissions in your
authreducer or an external service and store them in theUsertable, then check these simpler flags in your data reducers. - Not running
spacetime generate: Any changes toschema.spacetimedbrequirespacetime generateto update the Rust types, otherwise yourmod.rsmight have compilation errors. - Client-side
authreducer call: Remember that the client must explicitly call theauthreducer with their token. SpaceTimeDB does not automatically infer identity from a connection string.
Summary
Phew! We’ve covered a lot of ground today, laying the foundation for secure SpaceTimeDB applications. Here are the key takeaways:
- Authentication vs. Authorization: Authentication verifies who you are, authorization determines what you can do.
- SpaceTimeDB’s Security Core: Relies on the
callerobject, the specialauthreducer, and the internalidentitytable. - External Authentication: SpaceTimeDB integrates with external identity providers (Auth0, Firebase, etc.) for authenticating users and issuing tokens.
- The
authReducer’s Role: It validates external tokens, manages SpaceTimeDBIdentityentries (often by deriving a stableIdentityIdfrom an external user ID), and returns theIdentityIdto be associated with the client’s connection. - Reducer-based Authorization: Your data-modifying reducers use the
caller.identity_idto enforce granular access control, ensuring only authorized users can perform specific actions or modify specific data. - The
grant_accessStatement: A powerful schema-level tool for defining read permissions, preventing unauthorized clients from even subscribing to sensitive data.
You now have the tools to build real-time applications that are not just highly collaborative but also robustly secure. In the next chapters, we’ll continue to explore advanced topics, including deployment and scaling strategies, to bring your SpaceTimeDB applications to production readiness!
References
- SpaceTimeDB Official Documentation - Authentication
- SpaceTimeDB Official Documentation - Reducers
- SpaceTimeDB Official Documentation - Schema
- JSON Web Token (JWT) Official Website
- Auth0 Documentation - What is Authentication?
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.