Semaphor
Security

Token Integration

Generating tokens for embedded dashboards with Unified Security

When a connection uses Unified Security, the token request shifts from carrying security policy details to identifying who the user is. Admins define the policies; the token tells Semaphor which actor to resolve them for.

This page covers how to structure token requests for connections in unified mode. For the full token API reference, see Token Options and Tokens API.


What Changes in Unified Mode

In legacy mode, your token request carries security policies (cls, rcls, sls) with concrete parameter values. In unified mode, those policies live on the server as policy definitions and assignments.

Your token request needs to provide:

  1. Actor identity -- who is this user? (tenantId, endUserId, orgUserId)
  2. Security params (optional) -- values for any unresolved {{ placeholder }} in the policy definition templates, via the securityParams field

That's it. Semaphor resolves the effective security context from persisted assignments plus any securityParams you supply.

Prefer assignment-bound parameters when possible

For the simplest integration, bind parameter values directly in assignments. Your token request stays as simple as passing actor identity, and admins manage all security values in one place. Use securityParams when values genuinely vary per request — such as a user-selected filter or session-specific context.

Legacy fields are rejected on unified connections

If a connection is in unified mode, passing cls, rcls, or sls in the token request will fail with: "Unified Security runtime cutover does not support legacy token cls/rcls/sls overlays." Remove these fields from your request body for unified connections.


Actor Identification

The token request identifies the actor using the same fields as before. Semaphor derives the actor type from the fields you provide, checked in this order:

Fields ProvidedActor TypeDescription
orgUserIdORG_USEROrganization-level user
tenantId + endUserIdTENANT_USEREnd user within a specific tenant
tenantId aloneTENANTTenant-level actor (no specific user)

If none of these fields are present on a unified connection, the request fails with: "Unified Security requires an organization, tenant, or tenant user actor context."

These fields work on both dashboard tokens and project tokens. See Token Options for the complete field reference.


Integration Scenarios

The admin creates policy definitions and assignments with all parameter values bound. Your token request only needs actor identity.

const TOKEN_URL = 'https://semaphor.cloud/api/v1/token';
 
const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',       
  endUserId: 'user_jane',        
  // No cls, rcls, sls, or security params needed.
  // Admin configured everything via policy definitions and assignments.
};
 
async function fetchToken() {
  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(requestBody),
  });
 
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
 
  const token = await response.json();
  return token;
}

This is the simplest integration — and the recommended approach for most use cases. The admin manages all security values centrally, and your code has zero security logic.

Scenario 2: Admin Policy Definitions + Runtime Security Params

When some values genuinely vary per request and can't be pre-configured in assignments, you can supply them at token time via the securityParams field. The admin creates the policy structure and binds stable values in assignments; your code fills in the dynamic ones.

For example, the admin binds tenant_id in the assignment. You supply region and department at runtime because they change per session.

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',
  endUserId: 'user_jane',
  securityParams: {                   
    region: 'west',                   
    department: 'engineering',        
  },                                  
};
 
async function fetchToken() {
  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(requestBody),
  });
 
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
 
  return await response.json();
}

Security params fill unresolved {{ placeholder }} values in the policy definition templates. They can narrow access but cannot widen beyond what persisted assignments allow.

Scenario 3: No Assignments (Unrestricted Access)

If a connection is in unified mode but the actor has no matching assignments, the request proceeds with the base connection -- no Unified Security restrictions are applied.

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_new_customer',
  endUserId: 'user_bob',
  // This actor has no assignments yet.
  // They get unrestricted access on the base connection.
};

This is useful during incremental migration. You can switch a connection to unified mode and add assignments for actors over time. Actors without assignments continue using the connection as-is.

Audit before switching modes

Before switching a connection to unified, verify that all actors who need security restrictions have assignments. Use the Resolution Preview in the admin UI to check any actor's effective policy.


The securityParams Field

In unified mode, the securityParams field supplies values for unresolved {{ placeholder }} parameters in your policy definitions. If an admin left a placeholder unbound in the assignment, your token request fills it at runtime.

securityParams vs params

The securityParams field is for Unified Security placeholder values (CLS/SLS/RLS templates). The separate params field is for general runtime preferences like calendarContext, currencyFormat, and timezone. See Token Options for the full field reference.

const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',
  endUserId: 'user_jane',
  securityParams: {                   
    department: 'sales',              // fills {{ department }} placeholder  //
    access_tier: 'standard',          // fills {{ access_tier }} placeholder  //
  },                                  
};

Each key in securityParams matches a {{ placeholder }} name in the policy definition. If the placeholder uses @secret, see the next section for how secrets are handled.


Secret Parameters (@secret)

Policy definitions can mark sensitive placeholders with an @secret suffix -- for example, {{ password@secret }} in a CLS connection template. Secret values are passed in a separate secretSecurityParams field on the token request, not in securityParams. Semaphor encrypts them server-side so the plaintext never appears in the signed JWT.

How it works

  1. You send the plaintext secret in the secretSecurityParams field, using the plain key name (without the @secret suffix).
  2. Non-secret values go in securityParams as usual.
  3. Semaphor validates the secret against applicable CLS templates, stores it server-side, and replaces it with an opaque lookup reference.
  4. The signed JWT carries the reference, not the plaintext secret.
  5. At query time, Semaphor resolves the real secret server-side when rendering the CLS template.

The plaintext secret is never embedded in the token payload.

Example

Given this CLS policy definition:

CLS policy definition
{
  "connectionTemplate": "postgresql://{{ username }}:{{ password@secret }}@db.example.com/app"
}

Your token request separates plain values from secrets:

Token request
const response = await fetch('https://semaphor.cloud/api/v1/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'project',
    projectId: PROJECT_ID,
    projectSecret: PROJECT_SECRET,
    tenantId: 'tenant_acme',
    endUserId: 'user_123',
    securityParams: {                                  
      username: 'analytics_user',                      
    },                                                 
    secretSecurityParams: {                            
      password: 'super-secret-db-password',            
    },                                                 
  }),
});

The resulting JWT payload contains a reference for the secret, not the plaintext:

JWT payload (conceptual)
{
  "type": "project",
  "project_id": "p_123",
  "tenantId": "tenant_acme",
  "endUserId": "user_123",
  "securityParams": {
    "username": "analytics_user",
    "password": "9c4d2c91-5f2a-4d8d-b6d0-3d6d1d5d4e11"
  }
}

Notice that both values end up under securityParams in the JWT -- but the password is an opaque reference, not the plaintext. At query time, Semaphor resolves 9c4d2c91-... back to the real password server-side and renders the connection string.

Key naming

Always use the plain key name -- write password, not password@secret. The @secret suffix only appears in the policy definition template to tell Semaphor how to handle the value. Plain values go in securityParams; secret values go in secretSecurityParams.

Do not mix secret and plain keys

A key cannot appear in both securityParams and secretSecurityParams. If a key matches an @secret placeholder in a CLS template, it must be in secretSecurityParams -- passing it in securityParams will return an error.


Dashboard Tokens vs Project Tokens

Both token types work with Unified Security. The difference is scope, not security behavior.

Dashboard TokenProject Token
CredentialsdashboardId + dashboardSecretprojectId + projectSecret
Actor identitytenantId, endUserIdorgUserId, tenantId, endUserId
Security paramssecurityParams fieldsecurityParams field
Secret security paramssecretSecurityParams fieldsecretSecurityParams field
Security modeRespects per-connection modeRespects per-connection mode
Mixed modesWorks (per-connection)Works (per-connection)

When a project has some connections in legacy mode and others in unified mode, both are handled correctly within the same token. Each connection resolves security independently based on its own mode.


What NOT to Do

Do not mix legacy security fields with unified connections

The following token request will be rejected if the connection is in unified mode:

// This will fail on unified connections
const requestBody = {
  type: 'project',
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  tenantId: 'tenant_acme',
  endUserId: 'user_jane',
  cls: {                              
    name: 'store_sales_primary',      
    params: { tenant: 'acme' },       
  },                                  
  rcls: {                             
    name: 'region_filter',            
    params: { state: ['California'] },
  },                                  
};

Remove cls, rcls, and sls from your request body. Use the securityParams field for runtime security values instead.

If you need legacy security fields for some connections, keep those connections in legacy mode. Per-connection mode switching lets you migrate incrementally.


Best Practices

  • Bind stable values in assignments. If a value is fixed per actor — database name, tenant ID, schema — put it in the assignment. This keeps your token code simple and gives admins centralized control over security configuration.
  • Use securityParams for genuinely dynamic values. Values that change per request or per session (e.g., a user-selected region filter, a time-scoped access window) are a good fit for securityParams. There is no performance penalty for plain security params — they pass through to the token without additional processing.
  • Prefer assignment-bound secrets over token-time secrets. Secret parameters (@secret) passed via secretSecurityParams require server-side encryption and storage on every token request. Binding secrets in assignments avoids this per-request overhead and keeps sensitive values out of your application code entirely.

Error Reference

Common errors when using Unified Security with tokens:

Error MessageCauseSolution
"Unified Security requires an organization, tenant, or tenant user actor context"No actor identity fields in the token requestPass orgUserId, tenantId, or tenantId + endUserId
"Unified Security runtime cutover does not support legacy token cls/rcls/sls overlays"Legacy cls, rcls, or sls fields on a unified connectionRemove these fields; use securityParams for runtime security values
"placeholder 'X' is required but no value was provided"A policy definition placeholder has no value from assignments or securityParamsSupply the missing value in the securityParams field
"secret placeholder 'X' could not be resolved"A @secret parameter references an invalid or missing secretCheck the secret configuration in the admin UI
"Unified Security actor validation failed"The actor (org user, tenant, or tenant user) does not exist or does not belong to the projectVerify the actor exists and is associated with the project

Migration Checklist

When moving a connection from legacy to unified:

  1. Create policy definitions with the same policy logic you had in token-based CLS/RCLS/SLS.
  2. Create assignments for each actor, binding the parameter values that were previously in your token requests.
  3. Use the Resolution Preview to verify each actor's effective security matches your expectations.
  4. Remove cls, rcls, and sls fields from your token generation code. Move any runtime security values to securityParams.
  5. Switch the connection to unified mode.
  6. Test with each actor type to confirm correct behavior.

For a detailed migration walkthrough, see the Migration Guide.


Next Steps