JWT Authentication in Macro: Implementing Narrow-Scoped Document Permission Tokens
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. The macro_auth crate provides centralized validation through JwtValidationArgs and Axum middleware located in 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 and includes:
user_id: The Macro user identifierdocument_id: The target document UUIDexp: Expiration timestamp (set viaTOKEN_TTL_SECS = 3600)iss: Constant issuer string"document_storage_service"(defined asISSUER)
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 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.
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, the service extracts the token from query parameters and uses the generic decode helper with JwtValidationArgs containing the document-permission JWT secret.
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.
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:
- Single Document Restriction: The JWT only contains a single
document_idclaim, binding it to one specific resource - 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 - Short TTL: The
TOKEN_TTL_SECSconstant (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 during startup.
Summary
- Macro uses
macro_sync_service_jwtfor token generation andmacro_authfor validation across services DocumentPermissionTokencontains single-document scope with a 3600-second expiration defined byTOKEN_TTL_SECS- The document-storage-service mints tokens using the
encodehelper increate_permission_token.rs - The sync-service validates tokens via
decodeinauth.rsusingJwtValidationArgs - All services reuse the Axum middleware from
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. 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →