Skip to main content

Using Auth Claims

SpacetimeDB allows you to easily access authentication (auth) claims embedded in OIDC-compliant JWT tokens. Auth claims are key-value pairs that provide information about the authenticated user. For example, they may contain a user's unique ID, email, or authentication provider. If you want to view these fields for yourself, you can inspect the contents of any JWT using online tools like jwt.io.

Within a SpacetimeDB reducer, you can access the auth claims from a client's token via the ReducerContext. Below are some examples of how to use these claims.

Accessing Common Claims: Subject and Issuer

The subject (sub) and issuer (iss) are the most commonly accessed claims in a JWT. The issuer indicates which authentication provider issued the token, and the subject represents the unique identifier assigned to the user by the issuer. These are required claims, which are used to compute each user's Identity. Because these are so commonly used, there are helper functions to get them.

import { SenderError } from "spacetimedb/server";

spacetimedb.clientConnected((ctx) => {
  const jwt = ctx.senderAuth.jwt;
  if (jwt == null) {
    throw new SenderError("Unauthorized: JWT is required to connect");
  }
  console.info(`Client connected with sub: ${jwt.subject}, iss: ${jwt.issuer}`);
});

Example: Restricting auth providers

Since users can use any valid token to connect to SpacetimeDB, their token may originate from any authentication provider. For example, they could send an OIDC compliant token from GitHub even though you only want to accept tokens from Google. It often makes sense to restrict access to your module to users issued by your own issuer or specific issuers. It is best practice to check at least the issuer when clients connect, so you can ensure that your data can only be accessed by users of your application.

Additionally, it's imperative that you check the aud claim to ensure the issuer intended your application to receive the token. This ensures that applications which send you the token, cannot repurpose tokens issued for use with another application.

For example, we can restrict access to clients with SpacetimeAuth credentials.

// Set this to your the OIDC client (or set of clients) set up for your
// SpacetimeAuth project.
const OIDC_CLIENT_ID: &str = "client_XXXXXXXXXXXXXXXXXXXXXX";

#[reducer(client_connected)]
pub fn connect(ctx: &ReducerContext) -> Result<(), String> {
    let jwt = ctx.sender_auth().jwt().ok_or("Authentication required".to_string())?;
    if jwt.issuer() != "https://auth.spacetimedb.com/oidc" {
        return Err("Invalid issuer".to_string());
    }

    if !jwt.audience().iter().any(|a| a == OIDC_CLIENT_ID) {
        return Err("Invalid audience".to_string());
    }
    Ok(())
}

Accessing custom claims

If you want to access additional claims that aren't available via helper functions, you can parse the full JWT payload. This is useful for handling custom or application-specific claims.

As an example, let's say that your tokens have a "roles" claim, which is a list of priviledges. If you want to make sure that only users with the admin role are able to call a certain reducer, you could do the following:

Update your Cargo.toml like so to add serde and serde_json for parsing json:

[dependencies]
...

serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CustomClaims {
    pub roles: Vec<String>,
}

/// Returns Ok(()) if the sender has admin access, Err otherwise.
fn ensure_admin_access(sender_auth: &spacetimedb::AuthCtx) -> Result<(), String> {
    if sender_auth.is_internal() {
        // This is a scheduled reducer, so it should already be trusted.
        return Ok(());
    }
    let jwt = sender_auth.jwt().ok_or("Authentication required".to_string())?;
    let claims: CustomClaims = serde_json::from_slice(jwt.raw_payload().as_bytes()).map_err(|e| format!("Client connected with invalid JWT: {}", e).to_string())?;

    if claims.roles.iter().any(|r| r == "admin") {
        return Ok(());
    }
    Err("Admin role required".to_string())
}

#[spacetimedb::reducer]
pub fn admin_only_reducer(ctx: &ReducerContext) -> Result<(), String> {
    ensure_admin_access(&ctx.sender_auth())?;
    // Now we can safely perform admin-only actions.
    Ok(())
}

Summary and Best Practices

  • Always validate the presence and contents of JWT claims before trusting them in your application logic.
  • For custom application logic, deserialize the JWT payload to access additional claims which are not parsed by default.
  • Restrict accepted issuers where appropriate to enforce security policies.

For more information, refer to the SpacetimeDB documentation or reach out to the SpacetimeDB community for help.