Authorization
How IoTMan acts as an OAuth2 Authorization Server to protect your data endpoints and external services.
This guide explains IoTMan's authorization model and walks through two concrete scenarios: protecting an IoTMan data export endpoint and protecting an external HTTP server (nginx or Caddy) using the same tokens.
IoTMan's authorization model
IoTMan acts as an OAuth2 Authorization Server (AS). It issues signed tokens that protected resources — whether IoTMan's own data API or your own servers — can verify independently.
Three types of principals interact with the system:
| Principal | Who they are | How they authenticate |
|---|---|---|
| Workspace members | IoTMan users who manage the workspace | Passwordless magic link at IoTMan login |
| End-users | External people who consume protected resources | Passwordless magic link, via an OAuth2 Authorization Code + PKCE flow |
| M2M clients | Devices and backend services acting on their own | Client Credentials grant (client ID + secret) |
End-users and M2M clients are managed in the Access tab.
What a token carries
IoTMan issues JWT Bearer tokens signed with RS256 (asymmetric key). Any resource server can verify the signature using the public key from the JWKS endpoint — no shared secret required.
| Claim | Example value | Meaning |
|---|---|---|
iss | https://iotman.io | Issuer — always IoTMan |
sub | user-uuid or client-uuid | The end-user or M2M client the token was issued to |
aud | ws-uuid | The workspace UUID that issued the token |
scope | sensor:read video:gold | Space-separated list of granted permissions |
exp | 1753000000 | Expiry (Unix timestamp, typically +1 hour) |
aud = workspace UUID: Every token is bound to its workspace. A resource server validates aud against its known workspace UUID to prevent tokens from other workspaces from being accepted — even if they carry identical scope names.
Scopes as the access boundary: Scopes are defined per workspace. Each resource server decides which scope it requires. A token with sensor:read is useless against a server that requires video:gold.
Access token vs ID token: The token above is an access token — it proves authorization. IoTMan does not currently issue ID tokens.
Walkthrough: protecting an IoTMan export endpoint
Scenario: You have a workspace with a temperature sensor output. You want to restrict the data API endpoint so only authorised clients can read it.
1. Define a scope
In Access → Scopes, create:
- Name:
sensor:read - Description: Read access to sensor export endpoints
2. Protect the endpoint
In the Export tab, create or edit your endpoint and set Required scope to sensor:read. Without a valid Bearer token carrying this scope, the endpoint returns 401.
Endpoints without a required scope remain public — existing integrations are not affected.
3a. Grant access to an end-user
In Access → End-users, invite the user:
- Enter their email.
- Select scope:
sensor:read. - Click Invite. The user receives a magic link.
Register a PKCE OAuth2 client (in Access → OAuth2 clients) for the application that will request tokens on the end-user's behalf.
The end-user authenticates via Authorization Code + PKCE and the application exchanges the code for a token:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=<client-id>
&code=<auth-code>
&code_verifier=<pkce-verifier>
&redirect_uri=<redirect-uri>IoTMan returns a JWT with aud: <workspace-uuid> and scope: sensor:read.
3b. Grant access to an M2M client
Register a Client Credentials OAuth2 client in Access → OAuth2 clients. Copy the generated secret.
The device requests a token directly:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=<client-id>
&client_secret=<secret>
&scope=sensor:read4. Query the endpoint
GET https://iotman.io/data/out/<endpoint-id>
Authorization: Bearer <JWT>IoTMan validates the JWT: checks signature, aud (workspace UUID), scope, and exp. If all pass, data is returned.
Walkthrough: protecting an external server (nginx / Caddy)
Scenario: You serve video files through nginx. You want to use IoTMan tokens to control access by tier.
1. Define scopes
In Access → Scopes, create:
video:gold— access to gold-tier contentvideo:silver— access to silver-tier content
2. Grant access
Invite end-users in Access → End-users with the appropriate scope, or register a Client Credentials client for M2M access.
Your workspace UUID is shown in Access → Scopes. External servers use this as the expected aud value.
3. Configure nginx
nginx validates JWTs on each request. A minimal configuration using ngx_http_auth_jwt_module:
auth_jwt_jwks_uri https://iotman.io/.well-known/jwks.json;
location /gold/ {
auth_jwt "gold tier";
auth_jwt_claim_set $jwt_aud aud;
auth_jwt_claim_set $jwt_scope scope;
# Verify audience is the expected workspace
if ($jwt_aud != "<your-workspace-uuid>") { return 401; }
# Verify scope
if ($jwt_scope !~ "video:gold") { return 403; }
}3b. Configure Caddy
video.example.com {
forward_auth localhost:8181 {
uri /validate
copy_headers X-User X-Scope
}
file_server
}The sidecar fetches IoTMan's JWKS and validates aud (workspace UUID) and scope on each request.
JWKS reference
IoTMan exposes its public signing key at:
GET https://iotman.io/.well-known/jwks.jsonExample response:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "2026-key-1",
"n": "...",
"e": "AQAB"
}
]
}Most JWT libraries support JWKS-based verification out of the box. Examples:
# Python — PyJWT
from jwt import PyJWKClient, decode
jwks_client = PyJWKClient("https://iotman.io/.well-known/jwks.json")
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = decode(token, signing_key.key, algorithms=["RS256"],
audience="<your-workspace-uuid>")
# Verify the required scope is present
assert "sensor:read" in payload["scope"].split()// Node.js — jose
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://iotman.io/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, JWKS, {
audience: "<your-workspace-uuid>",
});
assert(payload.scope.split(" ").includes("sensor:read"));Critical: always verify both aud (workspace UUID) and the required scope. Verifying only the signature is insufficient.