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:

PrincipalWho they areHow they authenticate
Workspace membersIoTMan users who manage the workspacePasswordless magic link at IoTMan login
End-usersExternal people who consume protected resourcesPasswordless magic link, via an OAuth2 Authorization Code + PKCE flow
M2M clientsDevices and backend services acting on their ownClient 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.

ClaimExample valueMeaning
isshttps://iotman.ioIssuer — always IoTMan
subuser-uuid or client-uuidThe end-user or M2M client the token was issued to
audws-uuidThe workspace UUID that issued the token
scopesensor:read video:goldSpace-separated list of granted permissions
exp1753000000Expiry (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:

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:

  1. Enter their email.
  2. Select scope: sensor:read.
  3. 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:read

4. 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:

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.json

Example 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.