---
title: "Enterprise-managed authorization (EMA) with a workspace secret"
img: https://unified.to/images/logo.svg
date: 2026-09-22T00:00:00.000Z
updated: "2026-10-01T15:04:22.086Z"
tag: Guides
integrations: 
description: "Use your workspace signing secret to sign enterprise-managed authorization assertions. This is the brokered setup, your app signs a short-lived JWT, and..."
url: "https://docs.unified.to/guides/enterprise_managed_authorization_ema_with_a_workspace_secret"
---

# Enterprise-managed authorization (EMA) with a workspace secret
------
_September 22, 2026_

Use your workspace signing secret to sign enterprise-managed authorization assertions. This is the brokered setup: your app signs a short-lived JWT, and Unified.to checks it with the workspace jwt_secret. Your customers do not configure Unified.to in their identity provider.


> ⚠️ The secret shown by Copy on the API keys page is not the signing key. That value is a JWT used for webhook signatures. Assertions must be signed with jwt_secret, a separate hex string on the workspace.


## Before you start

- You are a workspace admin.
- Enterprise-managed authorization is for brokered issuers. Direct (XAA) issuers use a JWKS URL and a client ID allowlist, not the workspace secret.
- Regenerating the workspace secret creates `jwt_secret`. It also rotates the webhook signing secret, so existing webhook signature checks must be updated.

## 1. Turn the feature on


In the app, open Settings, then Enterprise Auth. Turn Enterprise-Managed Authorization on and save.


![Screenshot_2026-09-22_at_5.03.31_PM.png](https://s3.us-east-2.amazonaws.com/unified-article-images/enterprise_managed_authorization_ema_with_a_workspace_secret-0.png)


On EU or AU, use `api-eu.unified.to` or `api-au.unified.to`. The audience and resource claims later in this guide must use that same host.


## 2. Add a trusted issuer


On Enterprise Auth, choose Add trusted issuer and set:

- Topology: `Brokered (ISV)`
- Issuer (`iss`): the exact issuer string you will put in the assertion, for example `https://your-app.example`
- Key source: `Workspace secret`
- Allowed client IDs: leave blank
- Default scopes: the categories the token should receive when the assertion has no scope, for example `crm.read`
- Just-in-time identity mapping: on, if you want Unified.to to match the assertion email to a workspace user
- Enabled: on

    ![Screenshot_2026-10-01_at_11.02.09_AM.png](https://s3.us-east-2.amazonaws.com/unified-article-images/enterprise_managed_authorization_ema_with_a_workspace_secret-1.png)


![Screenshot_2026-09-22_at_5.12.26_PM.png](https://s3.us-east-2.amazonaws.com/unified-article-images/enterprise_managed_authorization_ema_with_a_workspace_secret-2.png)


The same record over the API:


```shell
curl -X POST https://api.unified.to/unified/enterprise-auth/trusted-issuers \
  -H "Authorization: Bearer $UNIFIED_WORKSPACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "isv_brokered",
    "issuer": "https://your-app.example",
    "key_source": "workspace_secret",
    "default_scopes": ["crm.read"],
    "jit_enabled": true,
    "enabled": true
  }'
```


## 3. Sign an assertion


Sign with HS256 and `jwt_secret`. The header `typ` must be `oauth-id-jag+jwt`. A plain JWT type is rejected.


```javascript
const jwt = require('jsonwebtoken');

const assertion = jwt.sign(
  {
    resource: 'https://api.unified.to',
    email: 'user@your-customer.com',
    scope: 'crm.read crm.write',
    jti: 'assertion-' + Date.now(),
  },
  process.env.WORKSPACE_JWT_SECRET,
  {
    algorithm: 'HS256',
    issuer: 'https://your-app.example',
    audience: 'https://api.unified.to',
    subject: 'customer-user-1',
    expiresIn: '5m',
    header: { alg: 'HS256', typ: 'oauth-id-jag+jwt' },
  }
);
```


Required claims:

- `iss` matches the trusted issuer string exactly
- `aud` is the API origin, such as https://api.unified.to
- resource is that same origin for REST calls, or that origin plus /mcp for MCP calls. A token for one resource is rejected by the other
- sub is the end user id in your app
- `jti` is unique for each redemption. The test panel does not consume jti
- scope is space-separated, such as `crm.read crm.write`. A comma-separated list is treated as one unknown scope and dropped
- email is a user who already belongs to the workspace, when just-in-time mapping is on

## 4. Test, then exchange


On the issuer row, choose Test and paste the assertion. A passing trace means the signature, issuer, audience, resource, and identity checks succeeded. Test does not issue a token.


Exchange the assertion once at the token endpoint. The access_token starts with ema_. Send it as Authorization: Bearer on integration data routes. It can call only the categories in its scopes. It cannot manage the workspace or create API keys.


```shell
curl -X POST https://api.unified.to/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=$ASSERTION"
```


### What comes back


The signed JWT is only the assertion. Do not put ema_ in front of it. Unified.to mints a separate opaque access token and returns it already prefixed.


```json
{
  "access_token": "ema_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "crm.read crm.write"
}
```


Sign a new assertion for every exchange. The same jti is rejected as a replay. The Test button checks the assertion and does not return an access token, and it does not consume jti.


> ⚠️ The token endpoint requires the workspace Enterprise Auth switch to be on. Test still runs when that switch is off. A disabled switch returns issuer not trusted.


### Call the API or MCP


The resource claim on the assertion chooses where this access token works. One token does not work for both. Send it as Authorization: Bearer followed by the access_token value.

- REST API: set resource to https://api.unified.to (or the EU or AU host). The token is accepted only on integration data routes such as GET /storage/{connection_id}/file, and only when the scope matches the category and the method (storage.read for GET, storage.write for writes).
- MCP: set resource to that same origin plus /mcp, for example https://api.unified.to/mcp. Send the bearer token to the MCP server. Add ?connection={connection_id} to run the call against one connection in the workspace.

```shell
curl https://api.unified.to/storage/$CONNECTION_ID/file \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```


```shell
curl "https://api.unified.to/mcp?connection=$CONNECTION_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'
```


A REST token sent to MCP is refused, and an MCP token sent to the REST API returns token issued for another resource. These tokens cannot manage the workspace, create API keys, or change enterprise-auth settings.


## 5. Map the user


The trusted issuer does not store a user id. Each assertion subject gets an identity row. With just-in-time mapping on, Unified.to sets the workspace user id when email matches a user in the workspace. Otherwise the row stays unmapped. Open Identities, choose Map on that subject, and enter the workspace user id. The identity row is created only after an assertion passes signature verification.