Skip to main content

Overview

Wacht JWT tokens contain claims that identify the user, their session, and their permissions within organizations and workspaces. Understanding these claims is essential for implementing proper authorization in your application.

Token Structure

Standard Claims

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenClaims {
    /// Issuer - identifies token source
    pub iss: String,

    /// Subject - user ID
    pub sub: String,

    /// Issued At - Unix timestamp
    pub iat: i64,

    /// Expiration - Unix timestamp
    pub exp: i64,

    /// Session ID - unique session identifier
    pub session_id: String,

    /// Organization context (optional)
    pub organization: Option<String>,

    /// Organization permissions (optional)
    pub organization_permissions: Option<Vec<String>>,

    /// Workspace context (optional)
    pub workspace: Option<String>,

    /// Workspace permissions (optional)
    pub workspace_permissions: Option<Vec<String>>,

    /// Additional custom claims
    #[serde(flatten)]
    pub custom_claims: serde_json::Map<String, serde_json::Value>,
}

Claim Descriptions

Core Claims

iss
string
Token issuer, typically your Wacht deployment URL (e.g., “https://app.wacht.io”)
sub
string
Subject identifier - the user’s unique ID in the system
iat
i64
Issued at time as Unix timestamp. Used to determine token age.
exp
i64
Expiration time as Unix timestamp. Token is invalid after this time.
session_id
string
Unique identifier for the user’s session. Used for session management and revocation.

Organization Claims

organization
string | null
Current organization ID if user has switched to an organization context
organization_permissions
string[] | null
List of permissions the user has within the organization. Examples:
  • users:read - View users
  • users:write - Manage users
  • billing:manage - Manage billing
  • settings:admin - Admin settings

Workspace Claims

workspace
string | null
Current workspace ID if user is in a workspace context
workspace_permissions
string[] | null
List of permissions the user has within the workspace. Examples:
  • projects:read - View projects
  • projects:write - Create/edit projects
  • content:manage - Manage content
  • members:invite - Invite members

Example Token Payload

Personal Context

User in their personal workspace:
{
  "iss": "https://app.wacht.io",
  "sub": "52057194421551105",
  "iat": 1699564800,
  "exp": 1699568400,
  "session_id": "session_2NK1qR5xPqPL",
  "organization": null,
  "organization_permissions": null,
  "workspace": null,
  "workspace_permissions": null
}

Organization Context

User switched to an organization:
{
  "iss": "https://app.wacht.io",
  "sub": "52057194421551105",
  "iat": 1699564800,
  "exp": 1699568400,
  "session_id": "session_2NK1qR5xPqPL",
  "organization": "org_2M5kD8nXpR",
  "organization_permissions": [
    "users:read",
    "users:write",
    "billing:read",
    "settings:read"
  ],
  "workspace": null,
  "workspace_permissions": null
}

Workspace Context

User in a specific workspace:
{
  "iss": "https://app.wacht.io",
  "sub": "52057194421551105",
  "iat": 1699564800,
  "exp": 1699568400,
  "session_id": "session_2NK1qR5xPqPL",
  "organization": "org_2M5kD8nXpR",
  "organization_permissions": ["users:read", "billing:read"],
  "workspace": "ws_3P7mF9qY",
  "workspace_permissions": [
    "projects:read",
    "projects:write",
    "content:manage",
    "analytics:view"
  ]
}

Using Claims in Your Application

Accessing Claims

After authentication, claims are available in the AuthContext:
use axum::Extension;
use wacht::middleware::AuthContext;

async fn handler(Extension(auth): Extension<AuthContext>) {
    // Basic user info
    println!("User ID: {}", auth.user_id);
    println!("Session: {}", auth.session_id);

    // Organization context
    if let Some(org_id) = &auth.organization_id {
        println!("Organization: {}", org_id);

        if let Some(perms) = &auth.organization_permissions {
            println!("Org permissions: {:?}", perms);
        }
    }

    // Workspace context
    if let Some(ws_id) = &auth.workspace_id {
        println!("Workspace: {}", ws_id);

        if let Some(perms) = &auth.workspace_permissions {
            println!("Workspace permissions: {:?}", perms);
        }
    }

    // Access full claims
    let claims = &auth.claims;
    println!("Issued at: {}", claims.iat);
    println!("Expires at: {}", claims.exp);
}

Permission Checking

fn has_permission(
    auth: &AuthContext,
    permission: &str,
    scope: PermissionScope
) -> bool {
    match scope {
        PermissionScope::Organization => {
            auth.organization_permissions
                .as_ref()
                .map(|perms| perms.contains(&permission.to_string()))
                .unwrap_or(false)
        }
        PermissionScope::Workspace => {
            auth.workspace_permissions
                .as_ref()
                .map(|perms| perms.contains(&permission.to_string()))
                .unwrap_or(false)
        }
    }
}

Permission Patterns

Common Organization Permissions

PermissionDescription
users:readView organization users
users:writeAdd/remove users
users:adminFull user management
billing:readView billing info
billing:manageUpdate payment methods
settings:readView org settings
settings:adminModify org settings
audit:viewView audit logs
admin:fullFull admin access

Common Workspace Permissions

PermissionDescription
projects:readView projects
projects:writeCreate/edit projects
projects:deleteDelete projects
content:readView content
content:writeCreate/edit content
content:publishPublish content
members:viewView members
members:inviteInvite new members
workspace:adminFull workspace control

Custom Claims

Adding Custom Claims

Custom claims can be added during token generation:
{
  "iss": "https://app.wacht.io",
  "sub": "52057194421551105",
  "iat": 1699564800,
  "exp": 1699568400,
  "session_id": "session_2NK1qR5xPqPL",
  "organization": "org_2M5kD8nXpR",
  "organization_permissions": ["users:read"],
  "workspace": null,
  "workspace_permissions": null,
  "custom_role": "developer",
  "feature_flags": ["new_ui", "beta_api"],
  "metadata": {
    "client_version": "2.0.0",
    "device_id": "device_123"
  }
}

Accessing Custom Claims

use serde_json::Value;

async fn handler(Extension(auth): Extension<AuthContext>) {
    // Access custom claims
    let claims = &auth.claims.custom_claims;

    // Get specific custom claim
    if let Some(role) = claims.get("custom_role") {
        if let Some(role_str) = role.as_str() {
            println!("Custom role: {}", role_str);
        }
    }

    // Check feature flags
    if let Some(Value::Array(flags)) = claims.get("feature_flags") {
        for flag in flags {
            println!("Feature flag: {:?}", flag);
        }
    }
}

Security Considerations

Token Size

  • Keep claims minimal to reduce token size
  • Large tokens can hit header size limits
  • Consider storing detailed permissions server-side

Sensitive Data

  • Never include passwords or secrets in claims
  • Avoid PII unless necessary
  • Claims are visible to anyone with the token

Permission Design

  • Use hierarchical permissions when possible
  • Implement least-privilege principle
  • Regular permission audits

Token Lifecycle

Token Refresh

When tokens expire, clients need to refresh:

Context Switching

When users switch organizations/workspaces:

Best Practices

  1. Validate All Claims - Don’t trust claims blindly
  2. Check Permissions - Always verify user has required permissions
  3. Handle Missing Claims - Claims might be null/absent
  4. Time Validation - Check iat/exp for token freshness
  5. Audit Access - Log permission checks for security

Next Steps