# JWT Authentication in Macro: Implementing Narrow-Scoped Document Permission Tokens

> Explore JWT authentication in Macro. Discover how narrow-scoped document permission tokens are minted and validated using a dual-crate architecture for enhanced security.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: how-to-guide
- Published: 2026-08-18

---

**Macro implements JWT authentication using a dual-crate architecture where the `macro_sync_service_jwt` crate mints single-document tokens with 3600-second TTLs, while the `macro_auth` crate validates them via reusable Axum middleware.**

Macro, the real-time document collaboration platform, uses a layered JWT authentication strategy to secure document access. The system separates generic macro-wide authentication from narrow-scoped document permission tokens, ensuring each token grants access to exactly one document for one user. This approach leverages the Rust crates `macro-auth` and `macro-sync-service-jwt` to handle validation and token generation respectively.

## Architecture of Macro's JWT Authentication System

The implementation spans two primary crates. The `macro_sync_service_jwt` crate defines the `DocumentPermissionToken` type and handles encoding/decoding logic in [`crates/macro_sync_service_jwt/src/lib.rs`](https://github.com/macro-inc/macro/blob/main/crates/macro_sync_service_jwt/src/lib.rs). The `macro_auth` crate provides centralized validation through `JwtValidationArgs` and Axum middleware located in [`crates/macro_auth/src/middleware/decode_jwt.rs`](https://github.com/macro-inc/macro/blob/main/crates/macro_auth/src/middleware/decode_jwt.rs).

This separation allows the document-storage-service to mint tokens while the sync-service validates them without sharing business logic.

### DocumentPermissionToken Structure

The token wraps a signed JWT containing minimal claims to authorize a single user for a single document. The claims structure lives in [`crates/documents/src/domain/permission_token.rs`](https://github.com/macro-inc/macro/blob/main/crates/documents/src/domain/permission_token.rs) and includes:

- `user_id`: The Macro user identifier
- `document_id`: The target document UUID  
- `exp`: Expiration timestamp (set via `TOKEN_TTL_SECS = 3600`)
- `iss`: Constant issuer string `"document_storage_service"` (defined as `ISSUER`)

## Minting Narrow-Scoped Tokens

When a client requests document access, the document-storage-service constructs a permission token. The implementation in [`services/document_storage_service/src/api/documents/permissions_token/create_permission_token.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/documents/permissions_token/create_permission_token.rs) builds the claims and invokes the `encode` helper from the JWT crate.

The service signs tokens using the **document-permission JWT secret** configured via `config.document_permission_jwt` or the `DOCUMENT_PERMISSION_JWT` environment variable.

```rust
use macro_sync_service_jwt::{encode, DocumentPermissionToken, ISSUER, TOKEN_TTL_SECS};
use chrono::{Utc, Duration};

#[derive(Serialize)]
struct DocumentPermissionClaims {
    sub: String,          // macro user id
    doc_id: String,       // document uuid
    iss: &'static str,    // always ISSUER
    exp: usize,           // epoch seconds
}

pub fn mint_permission_token(user_id: &str, doc_id: &str, secret: &str) -> anyhow::Result<DocumentPermissionToken> {
    let expires = Utc::now() + Duration::seconds(TOKEN_TTL_SECS as i64);
    let claims = DocumentPermissionClaims {
        sub: user_id.into(),
        doc_id: doc_id.into(),
        iss: ISSUER,
        exp: expires.timestamp() as usize,
    };
    encode(&claims, secret).map_err(Into::into)
}

```

## Validating Tokens in the Sync Service

The downstream sync-service validates these tokens on every WebSocket request. In [`services/sync-service/src/auth.rs`](https://github.com/macro-inc/macro/blob/main/services/sync-service/src/auth.rs), the service extracts the token from query parameters and uses the generic `decode` helper with `JwtValidationArgs` containing the document-permission JWT secret.

```rust
use macro_sync_service_jwt::decode;
use macro_auth::middleware::decode_jwt::JwtValidationArgs;

pub async fn verify_permission_token(
    token: &str,
    jwt_args: &JwtValidationArgs,
) -> Result<DocumentPermissionClaims, MacroAuthError> {
    // `jwt_args.document_permission_jwt` holds the secret for document tokens
    decode(token, jwt_args.document_permission_jwt.as_ref())
        .map_err(|e| MacroAuthError::JwtValidationFailed { details: e.to_string() })
}

```

The validation layer also enforces that the token's `iss` claim matches the expected `ISSUER` constant, preventing cross-service token replay.

## Reusable Middleware Across Services

All Macro HTTP services embed a reusable Axum middleware that builds a `JwtValidationArgs` instance from the environment. The middleware validates **MacroAccessTokens** (FusionAuth-derived) and **MacroApiTokens**; the same struct is reused for narrow-scoped tokens by passing the `document_permission_jwt` secret.

```rust
use axum::{extract::State, routing::get, Router};
use macro_auth::middleware::decode_jwt::{JwtValidationArgs, handler};

async fn protected_route(
    State(jwt_args): State<JwtValidationArgs>,
    axum::extract::TypedHeader(token): axum::extract::TypedHeader<BearerAuth>,
) -> Result<impl IntoResponse, MacroAuthError> {
    let claims = handler(&jwt_args, token.token())?; // validates macro‑access token
    // …logic using `claims`…
    Ok(Json(claims))
}

let app = Router::new().route("/protected", get(protected_route));

```

## Security Through Scope Limitation

Narrow scoping is enforced through three mechanisms defined in the source code:

1. **Single Document Restriction**: The JWT only contains a single `document_id` claim, binding it to one specific resource
2. **Unique Signing Secret**: The token is signed with a secret that is unique to the document-permission use-case (`document_permission_jwt`), distinct from macro-wide authentication secrets
3. **Short TTL**: The `TOKEN_TTL_SECS` constant (3600 seconds) limits the validity window

Because the JWT contains only the specific document ID and is signed with the dedicated secret, the token cannot be reused to access other documents or services.

## Configuration and Deployment

Services configure JWT validation through `JwtValidationArgs`, which supports two modes:

- **Compile-time constants** (`Comptime`) for local development
- **AWS Secrets Manager** integration via `JwtValidationArgs::new_with_secret_manager(environment, &secretsmanager_client)` for production deployments

The secret is stored in the environment as `DOCUMENT_PERMISSION_JWT` and injected into each service's [`main.rs`](https://github.com/macro-inc/macro/blob/main/main.rs) during startup.

## Summary

- Macro uses `macro_sync_service_jwt` for token generation and `macro_auth` for validation across services
- `DocumentPermissionToken` contains single-document scope with a 3600-second expiration defined by `TOKEN_TTL_SECS`
- The document-storage-service mints tokens using the `encode` helper in [`create_permission_token.rs`](https://github.com/macro-inc/macro/blob/main/create_permission_token.rs)
- The sync-service validates tokens via `decode` in [`auth.rs`](https://github.com/macro-inc/macro/blob/main/auth.rs) using `JwtValidationArgs`
- All services reuse the Axum middleware from [`macro_auth/src/middleware/decode_jwt.rs`](https://github.com/macro-inc/macro/blob/main/macro_auth/src/middleware/decode_jwt.rs)
- Secrets are managed through environment variables or AWS Secrets Manager via `JwtValidationArgs::new_with_secret_manager`

## Frequently Asked Questions

### What is the difference between MacroAccessTokens and DocumentPermissionTokens?

**MacroAccessTokens** are FusionAuth-derived tokens used for generic macro-wide authentication across all services, while **DocumentPermissionTokens** are narrow-scoped JWTs generated by the document-storage-service that grant access to only a single document for a single user. The document tokens use a separate signing secret and contain only the minimal claims needed for document access.

### How long are document permission tokens valid?

Document permission tokens are valid for **3600 seconds** (one hour) as defined by the `TOKEN_TTL_SECS` constant in [`macro_sync_service_jwt/src/lib.rs`](https://github.com/macro-inc/macro/blob/main/macro_sync_service_jwt/src/lib.rs). The expiration is encoded in the `exp` claim of the JWT and enforced during validation in the sync-service.

### Can a document permission token be used to access other services or documents?

No. Document permission tokens are cryptographically bound to a single document via the `document_id` claim and signed with a unique secret (`document_permission_jwt`) that is distinct from other Macro authentication secrets. Because the token contains only one document ID and the signature cannot be verified using other services' secrets, it cannot be reused for cross-document or cross-service access.

### Where is the JWT signing secret stored in production?

In production, the document-permission JWT secret is stored in **AWS Secrets Manager** and retrieved at runtime using `JwtValidationArgs::new_with_secret_manager`. Each service's [`main.rs`](https://github.com/macro-inc/macro/blob/main/main.rs) initializes the validation args by fetching the secret from the environment variable `DOCUMENT_PERMISSION_JWT` or via the Secrets Manager client, depending on the deployment configuration.