Control Plane Application Integration Guide

Version: 2.0

Audience: Application developers integrating with the Control Plane

Overview

The Control Plane is a centralized authentication and application launcher for Dotmatics AWS services. Users authenticate once with the Control Plane and can then launch registered applications directly from the UI. Applications are embedded inside the Control Plane using secure iframe isolation.

The Control Plane is the single JWT issuer. Every application session is bootstrapped through it — applications never issue their own tokens.

1. Architecture Overview

Cognito / API Key
  ↓
POST /auth/exchange  or  POST /auth/token
  ↓
Control Plane JWT  (RS256, 15 min)
  ↓
User selects app in Control Plane UI
  ↓
POST /auth/apps/{app_id}/launch   (Bearer: CP JWT)
  ↓
Response: { "launch_url": "https://billing.example.com?code=CODE&app_id=APP_ID" }
  ↓
Control Plane loads iframe pointing to launch_url
  ↓
Application receives ?code=CODE&app_id=APP_ID query params
  ↓
POST /auth/apps/exchange-code   (unauthenticated, rate-limited)
  Body: { "code": "...", "app_id": "..." }
  ↓
Response: { "access_token": "<scoped JWT>", "refresh_token": "...", "token_type": "bearer" }
  ↓
Application establishes session using the scoped access token
  ↓
When access token nears expiry: POST /auth/refresh  (refresh token)
  ↓
New rotated access token + refresh token (no re-launch required)

2. Application Requirements

RequirementDescription
HTTPSApplication must be served over HTTPS in production. http://localhost is allowed for local development only.
Embeddable via iframeMust allow embedding from the Control Plane domain. Must NOT set X-Frame-Options: DENY or SAMEORIGIN.
CSP frame-ancestorsMust set Content-Security-Policy: frame-ancestors https://<control-plane-domain>
Launch code supportMust read ?code and ?app_id query params on load and exchange them immediately.
URL policyExternal apps ("app_type": "external", default): the registered url must not point to private/internal IP ranges (10.x, 172.16–31.x, 192.168.x, loopback, link-local). http://localhost is allowed for local development only.

Internal apps ("app_type": "internal"): private network and RFC-1918 addresses are permitted. The AWS metadata endpoint (169.254.169.254) and unroutable addresses (0.0.0.0, ::1) remain blocked regardless of type.

3. Security Requirements

Content Security Policy (required)

Content-Security-Policy: frame-ancestors https://control-plane.example.com;

Replace control-plane.example.com with the actual Control Plane domain.

Must NOT be set:
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
Either of these will break iframe embedding.

HTTPS

Production application URLs must use https://. The Control Plane registration endpoint rejects non-HTTPS URLs for non-localhost addresses at registration time.

Recommended additional headers

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

4. App Registration

Applications must be registered via the Control Plane API before they can be launched. Registration creates a pending record that is reviewed by a global:application_manager or global:admin before becoming active.

Request

POST /auth/apps
Authorization: Bearer <CP JWT>
Content-Type: application/json

{
  "name": "Billing",
  "slug": "billing",
  "description": "Billing and subscription management",
  "url": "https://billing.example.com",
  "icon": "💰",
  "app_type": "external",
  "roles": [
    {"name": "billing:admin",  "description": "Full billing access"},
    {"name": "billing:viewer", "description": "Read-only billing access"}
  ],
  "groups": [
    {"name": "billing-admins", "description": "Billing administrators", "roles": ["billing:admin"]}
  ],
  "default_permissions": [
    {"identity_name": "admin@example.com", "roles": ["billing:admin"]}
  ]
}

App type

The app_type field controls URL validation at registration time:

ValueWhen to useURL restrictions
external (default)Publicly reachable applicationsPrivate/internal IP ranges blocked (SSRF protection)
internalApplications running inside a private network or VPCRFC-1918 addresses allowed; 169.254.x.x and 0.0.0.0 remain blocked

Naming rules (enforced at registration)

Status lifecycle

StatusMeaning
pendingSubmitted, awaiting review
approvedActive — roles/groups provisioned, app is launchable
rejectedRegistration denied
pending_updateApproved app with a proposed update awaiting review

On approval the Control Plane automatically provisions all roles and groups defined in the registration and applies default_permissions.

Updates are submitted via PATCH /auth/apps/{app_id}. The app remains launchable in its current approved state until the update is reviewed.

5. RBAC

The Control Plane uses Role-Based Access Control. Access to applications is governed by role assignments.

RoleDescription
global:adminFull control plane administration
global:application_managerCan review and approve/reject app registrations; sees all apps
auth:adminIdentity and role management
global:readRead-only access to control plane resources
{slug}:rolenameApp-scoped role (e.g. billing:admin, billing:viewer)
Users only see and can launch apps for which they hold at least one app-scoped role. global:application_manager and global:admin can see all apps regardless of app roles.

6. Launch Flow (step-by-step)

  1. Authenticated user selects an app in the Control Plane UI.
  2. Control Plane calls POST /auth/apps/{app_id}/launch with the user's Bearer JWT.
  3. Server verifies the user holds at least one role for the app, then generates a one-time opaque launch code.
  4. Server returns {"launch_url": "https://billing.example.com?code=CODE&app_id=APP_ID", "app_id": "..."}.
  5. Control Plane loads an iframe pointing to launch_url.
  6. The application page loads and reads the code and app_id query parameters.
  7. Application immediately calls POST /auth/apps/exchange-code with {"code": "...", "app_id": "..."}.
  8. Server validates the code (not expired, correct app_id, single-use) and deletes it atomically.
  9. Server returns a TokenResponse: {"access_token": "<scoped JWT>", "refresh_token": "...", "token_type": "bearer"}.
  10. Application stores both tokens (access token in memory or sessionStorage; refresh token in sessionStorage) and establishes its session.
  11. Before the access token expires, the application calls POST /auth/refresh with the refresh token to receive a new rotated access token and refresh token — no user interaction or re-launch required.
The launch code is consumed on first use. If the page reloads, the code in the URL is already spent. Applications should store both the access token and refresh token in sessionStorage. Use the refresh token to silently extend the session. Only re-initiate a launch (via the Control Plane) if the refresh token is expired or revoked.

7. Launch Code Properties

PropertyValue
FormatOpaque random string (not a JWT) — secrets.token_urlsafe(32)
TTL300 seconds (5 minutes)
UsageSingle-use — consumed and deleted on first exchange
BindingTied to a specific app_id and identity_id
StorageStored as an HMAC-SHA256 hash in DynamoDB; raw code is never persisted
URL params?code=CODE&app_id=APP_ID

8. exchange-code Response & Scoped JWT

POST /auth/apps/exchange-code returns a TokenResponse identical in shape to the standard token endpoints:

{
  "access_token": "<scoped JWT>",
  "refresh_token": "<opaque token>",
  "token_type": "bearer"
}

Token lifetimes are not included in the response — the access token's exp claim encodes its expiry, and the refresh token's expiry is enforced server-side (a 401 is returned when it is expired or revoked).

The access token is a standard RS256 JWT issued by the Control Plane KMS key. Its roles claim contains only the roles the user holds that are defined by the specific application. The refresh token is app-scoped — it can only produce new access tokens carrying the same app_id and the user's current app-scoped roles (re-resolved on each refresh).

{
  "iss": "https://control-plane.example.com",
  "sub": "identity-uuid",
  "iat": 1712345600,
  "exp": 1712346500,
  "jti": "uuid",
  "roles": ["billing:admin"],
  "app_id": "app-uuid"
}
PropertyValue
AlgorithmRS256 (signed by AWS KMS)
TTL900 seconds (15 minutes)
rolesOnly the user's roles that are scoped to this app (prefixed {slug}:)
app_idThe UUID of the registered application
Public keyGET /.well-known/jwks.json

9. Token Verification

  1. Fetch the JWKS document from GET /.well-known/jwks.json. Cache it for up to 1 hour.
  2. Verify the RS256 signature using the matching kid.
  3. Verify iss matches the expected Control Plane issuer.
  4. Verify exp has not passed.
  5. Verify app_id matches your application's registered ID.
  6. Use the roles claim for in-app authorization decisions.
GET /.well-known/jwks.json

Response:
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "kms-key-id",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "..."
    }
  ]
}

10. API Endpoints Reference

EndpointAuthPurpose
GET /auth/configNoneAuth discovery (Cognito config)
GET /.well-known/jwks.jsonNonePublic key for JWT verification
POST /auth/exchangeCognito tokenGet CP access token + refresh token
POST /auth/tokenAPI keyGet CP access token + refresh token
POST /auth/refreshRefresh tokenRotate access token (old refresh token consumed)
POST /auth/appsCP JWTRegister an application (creates pending record)
GET /auth/appsCP JWTList accessible apps (filtered by roles)
PATCH /auth/apps/{app_id}CP JWTSubmit an update to an approved app
POST /auth/apps/{app_id}/launchCP JWTGenerate a launch URL (requires app role)
POST /auth/apps/exchange-codeNone (rate-limited 10/min per IP)Exchange launch code for scoped access token + refresh token

11. Application Layout Guidelines

12. Security Best Practices