Access Control & Resource Sharing

Access tokens are used to authenticate and authorize requests to the API. They are usually bound to a resource, which can reside on any node within the Cloudillo network.

Token Types

Cloudillo uses different token types for different purposes:

AccessToken

Session tokens for authenticated API requests.

Purpose: Grant a client access to specific resources Format: JWT (JSON Web Token) Lifetime: 1-24 hours (configurable)

JWT Claims:

{
  "sub": "alice.example.com",   // Subject (user identity)
  "aud": "bob.example.com",     // Audience (target identity)
  "exp": 1738483200,            // Expiration timestamp
  "iat": 1738396800,            // Issued at timestamp
  "scope": "resource_id"        // Scope (resource identifier)
}

ActionToken

Cryptographically signed tokens representing user actions (see Actions.

Purpose: Federated activity distribution Format: JWT signed with profile key (ES384) Lifetime: Permanent (immutable, content-addressed)

ProxyToken

Inter-instance authentication tokens.

Purpose: Get access tokens from a remote instance (on behalf of a user) Format: JWT Lifetime: Short-lived (1-5 minutes)

JWT Claims:

{
  "sub": "alice.example.com",  // Requesting identity
  "aud": "bob.example.com",    // Target identity
  "exp": 1738400000,
  "iat": 1738396800,
  "scope": "resource_id"       // Scope (resource identifier)
}

Access Token Lifecycle

1. Token Request

Client requests an access token from their node.

2. Token Generation

The AuthAdapter creates a JWT with appropriate claims.

3. Token Validation

Every API request validates the token before processing.

4. Token Expiration

Tokens expire and must be refreshed.

Requesting an Access Token

When a user wants to access a resource, they follow this process:

  1. The user’s node requests an access token.
  2. If the resource is local, the node issues the token directly.
  3. If the resource is remote, the node authenticates with the remote node and requests a token on behalf of the user.
  4. The access token is returned to the user, allowing them to interact with the resource directly on its home node.

Security & Trust Model

  • Access tokens are cryptographically signed to prevent tampering.
  • Tokens have expiration times and scopes to limit misuse.
  • Nodes validate access tokens before granting access to a resource.

Example 1: Request access to own resource

sequenceDiagram
    box Alice frontend
        participant Alice shell
        participant Alice app
    end
    participant Alice node
    Alice shell ->>+Alice node: Initiate access token request
    Note right of Alice node: Create access token
    Alice node ->>+Alice shell: Access token granted
    deactivate Alice node
    Alice shell ->>+Alice app: Open resource with this token
    deactivate Alice shell
    Alice app ->+Alice node: Use access token
    loop Edit resource
        Alice app --> Alice node: Edit resource
    end
    deactivate Alice app
  • Alice opens a resource using her Cloudillo Shell
  • Her shell initiates an access token request at her node
  • Her node creates an access token and sends it to her shell
  • Her shell gives the access token to the App Alice uses to open the resource
  • The App uses the access token to edit the resource

Example 2: Request access to resource of an other identity

sequenceDiagram
    box Alice frontend
        participant Alice shell
        participant Alice app
    end
    participant Alice node
    participant Bob node
    Alice shell ->>+Alice node: Initiate access token request
    Note right of Alice node: Create signed request
    Alice node ->>+Bob node: Request access token
    Note right of Bob node: Verify signed request
    Note right of Bob node: Create access token
    deactivate Alice node
    Bob node ->>+Alice node: Grant access token
    deactivate Bob node
    Alice node ->>+Alice shell: Access token granted
    deactivate Alice node
    Alice shell ->>+Alice app: Open resource with this token
    deactivate Alice shell
    Alice app ->+Bob node: Use access token
    loop Edit resource
        Alice app --> Bob node: Edit resource
    end
    deactivate Alice app
    deactivate Bob node
  • Alice opens a resource using her Cloudillo Shell
  • Her shell initiates an access token request through her node
  • Her node creates a signed request and sends it to Bob’s node
  • Bob’s node creates an access token and sends it back to Alice’s node
  • Alice’s node sends the access token to her shell
  • Her shell gives the access token to the App Alice uses to open the resource
  • The App uses the access token to edit the resource

Token Validation Process

Authentication Middleware

Cloudillo uses Axum middleware to validate tokens on protected routes:

Handler Patterns:

Pattern 1: Required Authentication
async fn protected_handler(auth: Auth) -> Result<Response> {
    // auth.tn_id, auth.id_tag, auth.scope available
    // Access granted only if middleware validated token
}

Pattern 2: Optional Authentication
async fn public_handler(auth: Option<Auth>) -> Result<Response> {
    if let Some(auth) = auth {
        // Authenticated user - access Auth context
    } else {
        // Anonymous access - no Auth context
    }
}

The Axum extractor validates token before passing to handler.
If validation fails on required routes, request is rejected.

Validation Steps

When a request includes an Authorization: Bearer <token> header:

  1. Extract Token: Parse JWT from Authorization header
  2. Decode JWT: Parse header and claims (no verification yet)
  3. Verify Signature: Validate using AuthAdapter-stored secret
  4. Check Expiration: Ensure exp > current time
  5. Validate Claims: Check aud, scope, tid
  6. Create Auth Context: Build Auth struct for handler
pub struct Auth {
    pub tn_id: TnId,        // Tenant ID (database key)
    pub id_tag: String,     // Identity tag (e.g., "alice.example.com")
    pub scope: Vec<String>, // Permissions (e.g., ["read", "write"])
    pub token_type: TokenType,
}

Custom Extractors

Axum extractors provide typed access to authentication context:

TnId Extractor:

  • struct TnId(pub i64) - Wraps internal tenant ID
  • Usage: handler(TnId(tn_id): TnId) extracts from Auth context

IdTag Extractor:

  • struct IdTag(pub String) - Wraps user identity domain
  • Usage: handler(IdTag(id_tag): IdTag) extracts from Auth context

Auth Extractor (Full Context):

  • tn_id: Internal tenant identifier
  • id_tag: User identity (e.g., “alice.example.com”)
  • scope: Permission vector (e.g., [“read”, “write”])
  • token_type: Type of token (AccessToken, ProxyToken, etc.)

Usage: Check auth.scope.contains(&"write") for permission checks

Permission System

Cloudillo uses ABAC (Attribute-Based Access Control) for comprehensive permission management. Access tokens work in conjunction with ABAC policies to determine what actions users can perform.

Learn more: ABAC Permission System

Scope-Based Permissions

Access tokens include a scope claim that specifies permissions.

Resource-Level Permissions

Permissions are checked at multiple levels:

  1. File-Level: Who can access a file
  2. Database-Level: Who can access a database (RTDB)
  3. Action-Level: Who can see an action token
  4. API-Level: Rate limiting, quota enforcement

Permission Checking

Algorithm: Check Permission

Input: auth context, resource, required_scope
Output: Result<()>

1. Check token scope:
   - If required_scope NOT in auth.scope: Return PermissionDenied

2. Load resource metadata:
   - Fetch metadata by tn_id + resource_id

3. Check ownership:
   - If metadata.owner == auth.id_tag: Return OK (owner has all)

4. Check sharing list:
   - If auth.id_tag in metadata.shared_with: Return OK

5. Default: Return PermissionDenied

This pattern combines:
- Token-level permissions (scope)
- Resource-level ownership
- Resource-level sharing permissions

Cross-Instance Authentication

ProxyToken Flow

When Alice (on instance A) wants to access Bob’s resource (on instance B):

  1. Alice’s client requests access from instance A
  2. Instance A creates a ProxyToken signed with its profile key
  3. Instance A sends ProxyToken to instance B: POST /api/auth/proxy
  4. Instance B validates ProxyToken:
    • Fetches instance A’s public key
    • Verifies signature
    • Checks expiration
  5. Instance B creates AccessToken for Alice
  6. Instance B returns AccessToken to instance A
  7. Instance A returns AccessToken to Alice’s client
  8. Alice’s client uses AccessToken to access Bob’s resource directly on instance B

ProxyToken Verification

Algorithm: Verify ProxyToken

Input: JWT token string, requester_id_tag
Output: Result<ProxyTokenClaims>

1. Decode JWT without verification (read claims)

2. Fetch requester's profile:
   - GET /api/me from requester's instance
   - Extract public keys from profile

3. Find signing key:
   - Look up key by key_id (kid) in claims
   - If not found: Return KeyNotFound error

4. Verify signature:
   - Use requester's public key to verify JWT signature

5. Check expiration:
   - If exp < current_time: Return TokenExpired

6. Return verified claims

Token Generation

Creating an Access Token

Algorithm: Create Access Token

Input: tn_id, id_tag, resource_id, scope array, duration
Output: JWT token string

1. Build AccessTokenClaims:
   - sub: User identity (id_tag)
   - aud: Resource identifier (resource_id)
   - exp: current_time + duration
   - iat: current_time
   - scope: scope array joined as space-separated string
   - tid: Tenant ID (tn_id)

2. Sign JWT:
   - Use AuthAdapter to create JWT
   - Signed with instance's private key

3. Return token string

Token Refresh

Access tokens can be refreshed before expiration:

POST /api/auth/refresh
Authorization: Bearer <expiring_token>

Response:
{
  "access_token": "eyJhbGc...",
  "expires_in": 3600
}

API Reference

POST /api/auth/token

Request an access token.

Request:

POST /api/auth/token
Content-Type: application/json

{
  "resource_id": "f1~abc123...",
  "scope": "read write",
  "duration": 3600
}

Response (200 OK):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write"
}

POST /api/auth/refresh

Refresh an expiring access token.

Request:

POST /api/auth/refresh
Authorization: Bearer <current_token>

Response (200 OK):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3600
}

POST /api/auth/proxy

Request an access token on behalf of a user (cross-instance).

Request:

POST /api/auth/proxy
Content-Type: application/json
Authorization: Bearer <proxy_token>

{
  "user_id_tag": "alice.example.com",
  "resource_id": "f1~xyz789...",
  "scope": "read"
}

Response (200 OK):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3600
}

Security Considerations

Token Storage

Client-Side:

  • Store in memory or sessionStorage (not localStorage for better security)
  • Never log tokens
  • Clear on logout

Server-Side:

  • JWT signing secrets in AuthAdapter (never exposed)
  • Rotate signing secrets periodically
  • Use strong secrets (256+ bits)

Token Transmission

  • Always use HTTPS/TLS
  • Include in Authorization: Bearer <token> header
  • Never in URL query parameters

Token Revocation

Since JWTs are stateless, revocation requires:

  1. Short Lifetimes: Limit damage window (1-24 hours)
  2. Blacklisting: Maintain revoked token list (for critical cases)
  3. Key Rotation: Invalidates all tokens signed with old key

Rate Limiting

Protect token endpoints from abuse:

Token Endpoint Rate Limits:

  • MAX_TOKEN_REQUESTS_PER_HOUR: 100 (per user, per hour)

    • Prevents token request floods
    • Typical usage: <10 requests/hour
  • MAX_REFRESH_PER_TOKEN: 10 (per token)

    • Limits how many times a single token can be refreshed
    • Prevents refresh abuse
    • Encourages re-authentication after N refreshes

See Also

  • Identity System - Profile keys and cryptographic foundations
  • [Actions](/architecture/actions-federation/actions - Action tokens and federation
  • System Architecture - AuthAdapter and middleware