Skip to content

User tokens: the token route

When your app should call the File Service as the signed-in user (to browse folders, search, or upload under the user’s own permissions), it needs a user token for the Files API. Your backend gets it by token exchange and hands it out on a route of its own. This page is for backend developers adding that route. The backend-mediated flow of the quickstart needs none of it.

User scope: files:access

The token your app holds is for your API: its audience is your API, and the File Service rejects it with invalid_token. Your backend trades it for a token of the same user whose audience is files-api and whose scope is files:access. The exchanged token:

  • acts as the user: the user’s permissions apply, see Permissions;
  • is bound to your backend client’s tenant and application;
  • lives at most 10 minutes, so the app asks your route again before it expires;
  • is a plain Bearer token, also for a mobile app whose own token is key-bound.

It is the same route as for Chat and Notifications, with the other resource and scope.

  1. Give your backend client the exchange permission. Step 6 of the checklist: token exchange onto the Files API, files:access.

  2. Install the Identity Server client in your backend: @central-code-station/ccs-identity-client, from the same registry as the Files client.

  3. Add GET /files-token behind the check that protects the rest of your API:

    files-token.ts
    import { CcsIdentityClient, CcsOAuthError } from '@central-code-station/ccs-identity-client';
    import { filesEndpoints } from '@central-code-station/ccs-files-client';
    import type { CurrentUser } from '../shared/app';
    // The Files API and its resource identifier (never the audience), as FilesClient computes them: production unless
    // FILES_API_URL / FILES_API_RESOURCE or CCS_ENVIRONMENT say otherwise.
    const { apiUrl, resource } = filesEndpoints({ env: process.env });
    const ccsIdentity = new CcsIdentityClient({
    issuerUrl: process.env.CCS_ISSUER_URL!,
    clientId: process.env.CCS_FILES_CLIENT_ID!, // the backend client that holds the token-exchange permission files:access
    clientSecret: process.env.CCS_FILES_CLIENT_SECRET!,
    });
    // GET /files-token, behind your normal access-token check: the user's own token is already verified.
    export async function filesToken(user: CurrentUser): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {
    try {
    const { access_token, expires_in } = await ccsIdentity.exchangeToken({
    subjectToken: user.accessToken, // the token your app sent to your API
    resource, // https://files.centralcodestation.com/api in production
    scope: 'files:access',
    });
    return {
    status: 200,
    headers: { 'Cache-Control': 'no-store' },
    body: { access_token, expires_in, api_url: apiUrl, tenant_id: process.env.CCS_TENANT_ID, application_id: process.env.CCS_APPLICATION_ID },
    };
    } catch (error) {
    // invalid_grant (the user's token expired), unauthorized_client or invalid_target (setup): never retry blindly.
    if (error instanceof CcsOAuthError) return { status: 401, headers: {}, body: { error: error.error } };
    throw error;
    }
    }

    Answer Cache-Control: no-store. The optional api_url, tenant_id and application_id tell the app where to call, so it needs no configuration for the service.

The user clients do this for you: createTokenProvider and FilesClient.fromTokenRoute in the web client, TokenProvider and FilesClient.fromTokenRoute in the Flutter package. Any other client asks the route for a token, caches it until about a minute before expires_in runs out, and sends it as Authorization: Bearer to the API. Ask again on a 401. One request at a time is enough: share the pending request between callers.

Terminal window
curl -s "$FILES_API_URL/tenants/$CCS_TENANT_ID/applications/$CCS_APPLICATION_ID/me/capabilities" \
-H "Authorization: Bearer $USER_FILES_TOKEN"
# {"tenant_id":"fitness-co","application_id":"fitness-app","user_id":"user-ana","principal_type":"user","tenant_owner":false,"roles":[...],"capabilities":["files.view"]}

GET P/me/capabilities tells the app what the user may do everywhere in the application; every file and folder also carries my_actions, the user’s actions on that item.

Error Cause Fix
unsupported_grant_type Token exchange is off on the Identity Server Ask its operator to turn it on
unauthorized_client The client lacks the grant type or the exchange permission Step 6 of the checklist
invalid_target resource is the audience instead of the identifier, or Files is off for the application Send https://files.centralcodestation.com/api; check step 3
invalid_scope A scope other than files:access was asked for Only files:access can be exchanged
invalid_grant The user’s own token expired or was revoked The app signs the user in again