Skip to content

Conversation

jdetter
Copy link
Collaborator

@jdetter jdetter commented Sep 3, 2025

Description of Changes

This PR adds documentation for accessing auth claims in modules.

API and ABI breaking changes

This is a docs change.

Expected complexity level and risk

0

Testing

  • All examples compile and have been tested working

@jdetter jdetter force-pushed the jdetter/auth-claims-docs branch from d111139 to c5b0ff8 Compare September 3, 2025 06:33
@jdetter jdetter requested a review from jsdt September 5, 2025 17:07
Copy link
Contributor

@jsdt jsdt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look at the rust examples, and my main thoughts are that:

  1. We should never use unwrap, and
  2. We should probably have some example of a reducer other than the ClientConnected one.

```rust
#[reducer(client_connected)]
pub fn connect(ctx: &ReducerContext) -> Result<(), String> {
let auth_ctx = ctx.sender_auth();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generally better to avoid using unwrap, so it's more obvious that it can't panic.

Suggested change
let auth_ctx = ctx.sender_auth();
let jwt = ctx.sender_auth()
.jwt()
.ok_or_else(|| "Client connected without JWT".to_string())?;

If you think having those branches is more readable, you can also use a match:

Suggested change
let auth_ctx = ctx.sender_auth();
let jwt = match ctx.sender_auth().jwt() {
Some(jwt) => jwt,
None => return Err("Client connected without JWT".to_string()),
};
let jwt = match ctx.sender_auth()
.jwt()
.ok_or_else(|| "Client connected without JWT".to_string())?;

let claims: CustomClaims = serde_json::from_slice(payload.as_bytes()).map_err(|e| format!("Client connected with invalid JWT: {}", e).to_string())?;

// In this example you would really want to check the issuer of the JWT to ensure it is from a trusted provider
if auth_ctx.jwt().unwrap().issuer() != "https://accounts.google.com" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use claims.iss here instead of auth_ctx.jwt().unwrap().issuer().

pub struct User {
#[primary_key]
#[auto_inc]
pub id: u64,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using these auto_inc user ids in examples feels like it clashes with the identity that we are using in so many parts of our APIs.


## Example: automatically give users a role based on their auth claims

When users first connect to your SpacetimeDB module you may want to automatically give them some role. For example, at clockwork we might want to give users with a valid "clockworklabs.io" email an admin role. We can do this by checking the email claim in the connect reducer and giving them the admin role if they have a valid email.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the example of roles. I think it could use useful to add an example of using this in a reducer to decide whether a client is allowed to do something, since these examples are only covering the initial connection right now.

}

#[table(name = user_role)]
pub struct UserRole {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how useful these tables would be, since the reducers that would be enforcing any security rules would also have access to the email on the auth token. Having a table with email and role would make more sense to me.

Since the code in this example is computing the role from the email, we don't actually need to store any of the roles in tables. I think the main reason to have a user roles table would be to be able to set roles for different users via SQL or a different admin-only reducer.


let jwt = auth_ctx.jwt().unwrap();
// Example: We only accept google auth
if jwt.issuer() != "https://accounts.google.com" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One weird aspect of this is that people using maincloud have auth from auth.spacetimedb.com, so this would mean that someone logged in with the creds needed to publish would not be able to use spacetime sql.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[table(name = admin)]
pub struct Admin {
    #[unique]
    pub ident: Identity,
}

// Add to on_connect
#[reducer(init)]
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
    ctx.db.admin().insert(Admin{
        ident: ctx.sender
    });
    Ok(())
}

// Is the sender an owner of the db?
if ctx.db.admin().ident().find(ctx.sender).is_some() {
  return Ok(());
}
// Then check for google auth

// Note: check the audience here as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants