Version: 2.0
Audience: Application developers integrating with the Control Plane
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.
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)
| Requirement | Description |
|---|---|
| HTTPS | Application must be served over HTTPS in production. http://localhost is allowed for local development only. |
| Embeddable via iframe | Must allow embedding from the Control Plane domain. Must NOT set X-Frame-Options: DENY or SAMEORIGIN. |
CSP frame-ancestors | Must set Content-Security-Policy: frame-ancestors https://<control-plane-domain> |
| Launch code support | Must read ?code and ?app_id query params on load and exchange them immediately. |
| URL policy | External 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. |
Content-Security-Policy: frame-ancestors https://control-plane.example.com;
Replace control-plane.example.com with the actual Control Plane domain.
X-Frame-Options: DENY X-Frame-Options: SAMEORIGINEither of these will break iframe embedding.
Production application URLs must use https://. The Control Plane registration endpoint rejects non-HTTPS URLs for non-localhost addresses at registration time.
Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin
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.
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"]}
]
}
The app_type field controls URL validation at registration time:
| Value | When to use | URL restrictions |
|---|---|---|
external (default) | Publicly reachable applications | Private/internal IP ranges blocked (SSRF protection) |
internal | Applications running inside a private network or VPC | RFC-1918 addresses allowed; 169.254.x.x and 0.0.0.0 remain blocked |
slug must be lowercase alphanumeric with optional hyphens (e.g. my-app). It must not start or end with a hyphen.{slug}: (e.g. billing:admin).{slug} (e.g. billing-admins).roles list.default_permissions must exist in their respective lists.| Status | Meaning |
|---|---|
pending | Submitted, awaiting review |
approved | Active — roles/groups provisioned, app is launchable |
rejected | Registration denied |
pending_update | Approved 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.
The Control Plane uses Role-Based Access Control. Access to applications is governed by role assignments.
| Role | Description |
|---|---|
global:admin | Full control plane administration |
global:application_manager | Can review and approve/reject app registrations; sees all apps |
auth:admin | Identity and role management |
global:read | Read-only access to control plane resources |
{slug}:rolename | App-scoped role (e.g. billing:admin, billing:viewer) |
global:application_manager and global:admin can see all apps regardless of app roles.
POST /auth/apps/{app_id}/launch with the user's Bearer JWT.{"launch_url": "https://billing.example.com?code=CODE&app_id=APP_ID", "app_id": "..."}.launch_url.code and app_id query parameters.POST /auth/apps/exchange-code with {"code": "...", "app_id": "..."}.app_id, single-use) and deletes it atomically.TokenResponse: {"access_token": "<scoped JWT>", "refresh_token": "...", "token_type": "bearer"}.sessionStorage; refresh token in sessionStorage) and establishes its session.POST /auth/refresh with the refresh token to receive a new rotated access token and refresh token — no user interaction or re-launch required.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.
| Property | Value |
|---|---|
| Format | Opaque random string (not a JWT) — secrets.token_urlsafe(32) |
| TTL | 300 seconds (5 minutes) |
| Usage | Single-use — consumed and deleted on first exchange |
| Binding | Tied to a specific app_id and identity_id |
| Storage | Stored as an HMAC-SHA256 hash in DynamoDB; raw code is never persisted |
| URL params | ?code=CODE&app_id=APP_ID |
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"
}
| Property | Value |
|---|---|
| Algorithm | RS256 (signed by AWS KMS) |
| TTL | 900 seconds (15 minutes) |
roles | Only the user's roles that are scoped to this app (prefixed {slug}:) |
app_id | The UUID of the registered application |
| Public key | GET /.well-known/jwks.json |
GET /.well-known/jwks.json. Cache it for up to 1 hour.kid.iss matches the expected Control Plane issuer.exp has not passed.app_id matches your application's registered ID.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": "..."
}
]
}
| Endpoint | Auth | Purpose |
|---|---|---|
GET /auth/config | None | Auth discovery (Cognito config) |
GET /.well-known/jwks.json | None | Public key for JWT verification |
POST /auth/exchange | Cognito token | Get CP access token + refresh token |
POST /auth/token | API key | Get CP access token + refresh token |
POST /auth/refresh | Refresh token | Rotate access token (old refresh token consumed) |
POST /auth/apps | CP JWT | Register an application (creates pending record) |
GET /auth/apps | CP JWT | List accessible apps (filtered by roles) |
PATCH /auth/apps/{app_id} | CP JWT | Submit an update to an approved app |
POST /auth/apps/{app_id}/launch | CP JWT | Generate a launch URL (requires app role) |
POST /auth/apps/exchange-code | None (rate-limited 10/min per IP) | Exchange launch code for scoped access token + refresh token |
window.top, window.parent, or attempt to break out of the iframe.history.pushState should stay within the app).sessionStorage or memory, not localStorage (which persists across tabs and browser restarts).?code= value is consumed on first use and will return an error on replay.app_id in the JWT claims matches your registered application ID before trusting the token.POST /auth/refresh before it expires to receive a new rotated access token and refresh token. Only prompt the user to re-launch if the refresh token is expired or the request returns 401.exchange-code endpoint is limited to 10 requests per minute per IP. Do not retry aggressively on failure.