Netmon Docs · API Reference

Authentication & OAuthv7.0.20

The three ways to obtain a bearer token — session, personal access token, and OAuth — how to manage tokens once you hold them, and the discovery documents that let standards-based clients configure themselves.

Every call to the Netmon REST API is made on behalf of a user, and every call carries a bearer token in the Authorization header.

How authentication works

Netmon supports three credentials, all of which resolve to a bearer token you send as Authorization: Bearer <token>:

Every token acts as the user who created it. On every direct REST call the appliance enforces that user’s permissions — the permission:<slug> shown on each endpoint throughout this guide. Tokens also carry scopes (mcp:devices, mcp:logs, and so on), but scopes gate the separate MCP tool surface only; they do not restrict direct REST calls. A token’s REST reach is its owner’s permissions, full stop. The eight canonical scopes are listed in the OAuth sections below and in the Permissions & Scopes reference.

The permission:api gate. Minting a PAT, or completing any OAuth consent flow, requires the calling user to hold permission:api (or sa, which bypasses every permission). This is the single authorization axis shared by the PAT and OAuth paths — permission:api is can this user obtain a programmatic token at all, independent of what the token may then do.

Tag-restricted users see tag-scoped results from list endpoints throughout the API; that filtering is described per resource in later chapters and does not change how you authenticate.

Important

Treat every token as a password. Prefer the narrowest scope set a client needs rather than the full eight. Prefer browser sign-in (OAuth) for interactive clients so no long-lived secret is copied around. Set an expiry on PATs, and revoke any token you are no longer using — the DELETE /api/tokens/{id} and POST /api/oauth/token/revoke endpoints below exist for exactly this.

Signing in

Sign in and obtain a session token

POST/api/login

Permission none (public endpoint)

Authenticates a user with email and password and returns a bearer access token. This is the path the web UI uses for itself; it also establishes a browser web-guard session when called with cookies. For scripts, prefer a personal access token — this endpoint is throttled to 10 requests per minute and its token is tied to the SPA’s per-browser revocation behavior.

Request body

FieldTypeRequiredNotes
emailstringyesUsername or email. Max 255.
passwordstringyesThe account password.
totpstringnoOne-time MFA code. Required only on the second call when the first returns TOTP Required (see below).
browser_idstringnoPer-browser token name in the form spa:<uuid>:<fp>. The SPA sets this; omit it and the token is named authToken.

Response 200 (success — note the status field carries 201)

{
  "status": 201,
  "hasError": false,
  "user": { "id": 7, "email": "operator@example.com", "name": "Operator" },
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi…"
}

Response 200 (MFA required — resubmit with totp)

{ "status": false, "hasError": true, "message": "TOTP Required" }

Errors

StatusBodyWhen
200{"status":false,"hasError":true,"message":"Invalid Username/Password"}Bad credentials or invalid TOTP. The status field, not the HTTP code, signals failure.
422{"message":…,"errors":{…}}email or password missing.
429rate-limit responseMore than 10 attempts in a minute.
Response shape

Login does not use HTTP status to signal application errors — a failed sign-in still returns HTTP 200 with hasError: true. Read the body’s status/hasError fields, not the HTTP code.

Example

curl -sS -X POST https://APPLIANCE/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"operator@example.com","password":"s3cret"}'

Sign out

POST/api/logout

Permission any authenticated user

Revokes the access token used to make the call and tears down the companion web-guard session. After this returns, the token is dead.

Response 200

{ "message": "You have been successfully logged out." }

Example

curl -sS -X POST https://APPLIANCE/api/logout -H "Authorization: Bearer $TOKEN"

Rehydrate the browser session

POST/api/ensureWebSession

Permission any authenticated user

A helper used by the web UI: given a valid bearer token, it re-establishes the browser’s web-guard session cookie so a server-rendered consent page (such as /auth/authorize) recognizes the user without a second login. Bodyless; integrators rarely call it directly.

Response 200

{ "status": true }

Errors

StatusBodyWhen
401{"status":false,"message":"unauthenticated"}No valid token.
500{"status":false,"message":"no session context"}Called without a session-capable request context.

Server information

POST/api/information

Permission none (public endpoint)

Returns the appliance hostname and whether a PHP session is present. Useful as a reachability probe before authenticating. Bodyless.

Response 200

{
  "status": 201,
  "hasError": false,
  "data": { "hostname": "netmon-prod", "sessionId": false }
}

Example

curl -sS -X POST https://APPLIANCE/api/information

Personal access tokens

Personal access tokens are the recommended credential for scripts and automation. You mint one, the plaintext is shown once, and you store it like any other secret. Tokens default to a 365-day lifetime and carry the full eight-scope set unless you narrow either at creation. All three routes are gated by permission:api and operate only on the calling user’s own tokens.

Two name prefixes are reserved and rejected at creation: spa: (the web UI’s per-browser token namespace) and MCP Client: (the prefix OAuth flows stamp on issued tokens). Choose any other name.

List your tokens

GET/api/tokensapi

Lists the calling user’s tokens. Recently-revoked rows (created within the last 30 days) are included so the UI can show history; older revoked rows are hidden. The plaintext token value is never returned by this endpoint — only by store.

Response 200

{
  "tokens": [
    {
      "id": 42,
      "name": "nightly-report-runner",
      "origin": "pat",
      "client_id": null,
      "scopes": ["mcp:devices", "mcp:reports"],
      "revoked": false,
      "expires_at": "2027-06-15 12:00:00",
      "created_at": "2026-06-15 12:00:00",
      "last_used_at": "2026-06-15 13:04:11"
    }
  ]
}

The origin field is oauth for tokens minted through an OAuth flow (name begins MCP Client: ) and pat for everything else.

Errors

StatusBodyWhen
500{"error":"Failed to load tokens"}Unexpected server error.

Example

curl -sS https://APPLIANCE/api/tokens -H "Authorization: Bearer $TOKEN"

Mint a personal access token

POST/api/tokensapi

Creates a new token and returns its plaintext value in the response. Save it now — it is never shown again.

Request body

FieldTypeRequiredNotes
namestringyes1–100 chars. Must not start with spa: or MCP Client: .
expires_in_daysintegerno1–3650. Defaults to 365.
scopesarraynoSubset of the eight mcp:* scopes. An empty or omitted array defaults to the full set.
scopes.*stringEach must be one of mcp:devices, mcp:alerts, mcp:logs, mcp:reports, mcp:system, mcp:tools, mcp:vne, mcp:capture.

Response 201

{
  "id": 43,
  "name": "nightly-report-runner",
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi…",
  "expires_at": "2027-06-15T12:00:00+00:00",
  "created_at": "2026-06-15T12:00:00+00:00"
}

Errors

StatusBodyWhen
400{"errors":{"name":["…reserved prefix…"]}}Name uses a reserved prefix.
400{"errors":{…}}Validation failed (bad name length, out-of-range expiry, unknown scope).
500{"error":"Failed to create token"}Unexpected server error.

Example

curl -sS -X POST https://APPLIANCE/api/tokens \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"nightly-report-runner","expires_in_days":730,"scopes":["mcp:devices","mcp:reports"]}'

Revoke one of your tokens

DELETE/api/tokens/{id}api

Revokes a token you own. The ownership check is built in — you can only revoke your own tokens through this route.

Path parameters

NameTypeNotes
idintegerToken id from the listing.

Response 200

{ "revoked": true }

Errors

StatusBodyWhen
404{"error":"Token not found"}No token with that id is owned by you.
500{"error":"Failed to revoke token"}Unexpected server error.

Example

curl -sS -X DELETE https://APPLIANCE/api/tokens/43 \
  -H "Authorization: Bearer $TOKEN"

Administering other users’ tokens

These three routes let an administrator audit and revoke tokens belonging to other users — for credential rotation or when an account is compromised. They live under permission:users (an sa or users holder) rather than permission:api, so an admin can manage tokens without personally holding the API gate.

List another user’s tokens

GET/api/users/{userId}/tokensusers

Same shape and filtering as GET /api/tokens, but for the user named in the path.

Path parameters

NameTypeNotes
userIdintegerThe target user’s id.

Response 200 — identical {"tokens":[…]} envelope as the self-listing.

Errors

StatusBodyWhen
403{"error":"Forbidden"}Caller lacks sa/users.
404{"error":"User not found"}No such user.
500{"error":"Failed to load tokens"}Unexpected server error.

Revoke one of another user’s tokens

DELETE/api/users/{userId}/tokens/{id}users

Revokes a single token belonging to the named user. The revoking admin’s id is recorded for audit.

Path parameters

NameTypeNotes
userIdintegerThe token owner’s id.
idintegerToken id.

Response 200

{ "revoked": true }

Errors

StatusBodyWhen
403{"error":"Forbidden"}Caller lacks sa/users.
404{"error":"Token not found"}No matching token for that user.
Destructive

This immediately invalidates the target user’s token. Any client using it starts failing on the next call.

Revoke all of a user’s tokens

POST/api/users/{userId}/tokens/revoke-allusers

The kill switch: revokes every live token for the named user in one shot. Used when an account is compromised or being deprovisioned. Returns the count of tokens newly revoked (already-revoked rows are skipped). Bodyless.

Path parameters

NameTypeNotes
userIdintegerThe target user’s id.

Response 200

{ "revoked": 3, "user_id": 12 }

Errors

StatusBodyWhen
403{"error":"Forbidden"}Caller lacks sa/users.
404{"error":"User not found"}No such user.
Destructive

Every active token for the user is revoked at once, including their web UI session. They will need to sign in again.

OAuth 2.0 device-code flow

The device-code flow (RFC 8628) is how a non-browser client — the PowerShell module, the Wireshark extcap satellite, an MCP client — gets a token when its user must consent in a browser. The client asks for a device_code and a short user_code, displays the user_code and a verification URL to the user, and polls while the user approves in any browser. These endpoints are public but rate-limited (30 requests/minute on the /api/oauth/* group). The browser consent pages live under /auth/device.

The full round trip:

  1. Client POSTs to /api/oauth/device/authorize, receives device_code, user_code, and verification_uri.
  2. Client tells the user to open verification_uri and enter user_code.
  3. User (signed in, holding permission:api) approves at /auth/device.
  4. Client polls /api/oauth/device/token with the device_code every interval seconds until it gets an access token.

Request a device code

POST/api/oauth/device/authorize

Permission none (public endpoint)

Starts the flow. Returns the codes and the verification URL the client shows to the user.

Request body

FieldTypeRequiredNotes
client_namestringyesPrintable ASCII, max 100. Shown to the user on the consent page (e.g. powershell-host).
scopesarraynoSubset of the eight mcp:* scopes. Defaults to ["mcp:devices"].

Response 200

{
  "device_code": "Hq3…40chars…",
  "user_code": "ABCD1234",
  "verification_uri": "https://APPLIANCE/auth/device",
  "verification_uri_complete": "https://APPLIANCE/auth/device?user_code=ABCD1234",
  "expires_in": 600,
  "interval": 5
}

Example

curl -sS -X POST https://APPLIANCE/api/oauth/device/authorize \
  -H "Content-Type: application/json" \
  -d '{"client_name":"powershell-host","scopes":["mcp:devices","mcp:logs"]}'

Poll for the token

POST/api/oauth/device/token

Permission none (public endpoint)

Exchanges an approved device_code for an access token. Until the user has approved, this returns authorization_pending — keep polling at the interval returned above. The code is single-use: once it yields a token it is consumed.

Request body

FieldTypeRequiredNotes
device_codestringyesThe device_code from the authorize call.

Response 200

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi…",
  "refresh_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mcp:devices mcp:logs"
}

Errors

StatusBodyWhen
400{"error":"authorization_pending","error_description":"…"}User has not approved yet. Keep polling.
400{"error":"expired_token","error_description":"…"}The device code expired or was already used.

Example

curl -sS -X POST https://APPLIANCE/api/oauth/device/token \
  -H "Content-Type: application/json" \
  -d '{"device_code":"Hq3…40chars…"}'

GET/auth/device

Permission browser web session (not a bearer token)

The HTML page where a signed-in user enters the user_code to approve a device. Accepts an optional ?user_code= query parameter to pre-fill the field (used by verification_uri_complete). If the browser has no web-guard session, it redirects to the SPA login with a ?redirect= back to this URL. This page is opened in a browser by a human, not called by an API client.

Approve a device

POST/auth/device

Permission browser web session (not a bearer token); user must hold api or sa

The form submission from the consent page. Validates that the signed-in user holds permission:api, looks up the user_code, mints the token, and stores it against the device_code so the polling client can collect it. Renders a success page on approval. Submitted by the browser, not by an API client.

Request body (form-encoded)

FieldTypeRequiredNotes
user_codestringyesExactly 8 characters.

Errors — re-renders the form with an error message when the user lacks permission:api, the code is invalid/expired, or the code was already used. The message is intentionally identical across the permission-denied and invalid-code cases so it cannot be used to probe for active codes.

Device-code input helper script

GET/auth/device-code.js

Permission none (public endpoint)

Serves a small same-origin JavaScript asset that auto-formats the user_code input on the consent page (uppercases, strips non-alphanumerics). It exists as an external file so the SPA’s strict script-src 'self' CSP allows it. Not an API endpoint — the consent page loads it.

Response 200Content-Type: application/javascript, cached for one day.

OAuth 2.1 authorization-code flow with PKCE

The authorization-code flow (per the MCP 2025-03-26 spec) is the browser redirect flow used by clients that can host a redirect URI — typically a loopback http://localhost:… or an HTTPS callback. PKCE with the S256 method is mandatory: the client generates a code_verifier, sends its SHA-256 challenge to /auth/authorize, and proves possession of the verifier at /auth/token. Redirect URIs must be localhost/loopback or HTTPS.

A client may pre-register through Dynamic Client Registration (below) to obtain a client_id, or use a known one. Registered clients must be approved by an operator before they can complete the flow.

Authorization request (consent page)

GET/auth/authorize

Permission browser web session (not a bearer token); user must hold api or sa

The browser endpoint a client redirects the user to. It validates the request parameters, ensures the user is signed in (bouncing to SPA login with ?redirect= if not), checks permission:api, confirms the client is approved, and renders the consent page listing the requested scopes. This URL is opened in the user’s browser, not called by the API client directly.

Query parameters

NameTypeRequiredNotes
response_typestringyesMust be code.
client_idstringyesThe client identifier.
redirect_uristringyesMust be localhost/loopback or HTTPS, and match the client’s registered URIs if registered.
code_challengestringyesPKCE challenge (base64url of SHA256(code_verifier)).
code_challenge_methodstringnoDefaults to S256 (the only fully supported method).
scopestringnoSpace-delimited mcp:* scopes. Defaults to mcp:devices. Unknown scopes are rejected.
statestringnoOpaque value round-tripped back to the redirect URI.

Errors

StatusBodyWhen
400{"error":"unsupported_response_type",…}response_type is not code.
400{"error":"invalid_request",…}Missing client_id/redirect_uri/code_challenge, or a bad redirect-URI scheme/host.
400{"error":"invalid_scope",…}A requested scope is not in the canonical eight.
403consent page (auth.denied)User lacks permission:api.
403consent page (auth.pending)Client is registered but not yet operator-approved.

Authorization approval

POST/auth/authorize

Permission browser web session (not a bearer token); user must hold api or sa

The consent form submission. On approval it mints a one-time authorization code (10-minute expiry) and redirects the browser to the client’s redirect_uri with code and state. On denial — or any failed gate — it redirects with an OAuth error parameter instead. Submitted by the browser.

Request body (form-encoded)

FieldTypeRequiredNotes
client_idstringyesMust be an approved registered client.
redirect_uristringyeshttp/https; matched against the cached request.
code_challengestringyesPKCE challenge.
code_challenge_methodstringyesMust be S256.
scopestringyesSpace-delimited mcp:* scopes.
statestringnoRound-tripped to the callback.
denyanynoIf present, the user denied; redirect carries error=access_denied.

Response302 redirect to redirect_uri?code=…&state=… on approval, or redirect_uri?error=…&state=… on denial / a failed permission or approval gate.

Token exchange

POST/auth/token

Permission none (this endpoint is the auth mechanism); throttled 30/minute

Exchanges an authorization code for an access token, verifying PKCE. Also handles grant_type=refresh_token by delegating to the refresh path. The authorization code is consumed atomically (no replay). For confidential clients (token_endpoint_auth_method=client_secret_post), the client_secret is also verified.

Request body

FieldTypeRequiredNotes
grant_typestringyesauthorization_code or refresh_token.
codestringfor code grantThe authorization code from the redirect.
redirect_uristringfor code granthttp/https; must match the code’s stored value.
client_idstringfor code grantMust match the code’s client.
code_verifierstringfor code grantThe PKCE verifier; its S256 hash must equal the stored challenge.
client_secretstringconfidential clients onlyVerified for client_secret_post clients.
refresh_tokenstringfor refresh grantSee /api/oauth/token/refresh.

Response 200

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…",
  "scope": "mcp:devices mcp:logs"
}

Errors

StatusBodyWhen
400{"error":"unsupported_grant_type",…}grant_type is neither supported value.
400{"error":"invalid_grant",…}Code invalid/expired, client_id or redirect_uri mismatch, PKCE verification failed, or user not found.
401{"error":"invalid_client",…}A confidential client’s client_secret is wrong.
403{"error":"access_denied",…}The user no longer holds permission:api.

Example (authorization-code grant)

curl -sS -X POST https://APPLIANCE/auth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"authorization_code","code":"…","redirect_uri":"http://localhost:7777/callback","client_id":"…","code_verifier":"…"}'

Dynamic client registration

POST/auth/register

Permission none (public, per RFC 7591); throttled 10/minute

Registers an OAuth client and returns a generated client_id (and a client_secret for confidential clients). Registration is open per RFC 7591, but a freshly-registered client cannot complete an authorization-code flow until an operator approves it (operators review the pending queue under permission:system). Redirect URIs must be localhost/loopback or HTTPS. The appliance caps total registered clients at 500.

Request body

FieldTypeRequiredNotes
client_namestringyesPrintable ASCII, max 100.
redirect_urisarraynoEach must be http(s) and localhost/loopback or HTTPS.
grant_typesarraynoEach of authorization_code, refresh_token. Defaults to both.
response_typesarraynoEach must be code. Defaults to ["code"].
token_endpoint_auth_methodstringnonone (public) or client_secret_post (confidential). Defaults to none.

Response 201

{
  "client_id": "a1b2c3d4-…-uuid",
  "client_name": "My MCP Client",
  "redirect_uris": ["http://localhost:7777/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

For confidential clients the response also carries client_secret and client_secret_expires_at: 0 (never expires). The secret is shown once.

Errors

StatusBodyWhen
400{"error":"invalid_redirect_uri",…}A redirect URI is neither localhost nor HTTPS.
422{"message":…,"errors":{…}}Validation failed (bad name, scheme, grant/response type).
503{"error":"server_error",…}The 500-client cap is reached.

Example

curl -sS -X POST https://APPLIANCE/auth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"My MCP Client","redirect_uris":["http://localhost:7777/callback"]}'

Managing tokens (introspect, refresh, revoke)

These endpoints live under the /api/oauth/* group (throttled 30/minute) and let a client validate, renew, and discard the tokens it holds.

Introspect a token

POST/api/oauth/token/introspect

Permission none (public endpoint)

Validates a token and returns its metadata, in the RFC 7662 shape. Active tokens also have their last_used_at bumped as a side effect. An invalid, revoked, or expired token simply returns {"active": false}.

Request body

FieldTypeRequiredNotes
tokenstringyesThe bearer token (JWT) to inspect.

Response 200 (active)

{
  "active": true,
  "client_id": "a1b2c3d4-…",
  "username": "operator@example.com",
  "sub": 7,
  "scope": "mcp:devices mcp:logs",
  "exp": 1781000000
}

Response 200 (inactive)

{ "active": false }

Refresh a token

POST/api/oauth/token/refresh

Permission none (public endpoint)

Exchanges a valid refresh token for a new access token and a new refresh token. The refresh token is rotated — the one you submit is revoked, and the old access token it was bound to is revoked too. (grant_type=refresh_token at /auth/token routes here.)

Request body

FieldTypeRequiredNotes
refresh_tokenstringyesA live, unexpired refresh token.

Response 200

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "…new…",
  "scope": "mcp:devices mcp:logs"
}

Errors

StatusBodyWhen
400{"error":"invalid_grant",…}Refresh token invalid, expired, already used, or its user is gone.

Example

curl -sS -X POST https://APPLIANCE/api/oauth/token/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"…"}'

Revoke a token

POST/api/oauth/token/revoke

Permission any authenticated user

Revokes the supplied token and any refresh tokens bound to it. The caller must be authenticated and must either own the token or hold sa; revoking someone else’s token without sa is refused.

Request body

FieldTypeRequiredNotes
tokenstringyesThe bearer token (JWT) to revoke.

Response 200

{ "revoked": true }

Errors

StatusBodyWhen
400{"revoked":false}The token couldn’t be parsed or was already inactive.
403{"revoked":false}Caller neither owns the token nor holds sa.
Destructive

The token and its refresh tokens are immediately invalidated.

Example

curl -sS -X POST https://APPLIANCE/api/oauth/token/revoke \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOi…"}'

Discovery documents

Standards-based OAuth clients configure themselves from these two well-known documents rather than hardcoding endpoint URLs. Both are public, served on the appliance origin, and require no token.

Authorization server metadata

GET/.well-known/oauth-authorization-server

Permission none (public endpoint)

RFC 8414 authorization-server metadata. An MCP or OAuth client reads this to discover the authorize, token, and registration endpoints, the supported grant and challenge methods, and the scope list.

Response 200

{
  "issuer": "https://APPLIANCE",
  "authorization_endpoint": "https://APPLIANCE/auth/authorize",
  "token_endpoint": "https://APPLIANCE/auth/token",
  "registration_endpoint": "https://APPLIANCE/auth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "scopes_supported": [
    "mcp:devices", "mcp:alerts", "mcp:logs", "mcp:reports",
    "mcp:system", "mcp:tools", "mcp:vne", "mcp:capture"
  ]
}

Protected resource metadata

GET/.well-known/oauth-protected-resource

Permission none (public endpoint)

RFC 9728 protected-resource metadata. It points clients at the MCP resource (/mcp) and the authorization server that issues tokens for it, and lists the supported scopes.

Response 200

{
  "resource": "https://APPLIANCE/mcp",
  "authorization_servers": ["https://APPLIANCE"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "mcp:devices", "mcp:alerts", "mcp:logs", "mcp:reports",
    "mcp:system", "mcp:tools", "mcp:vne", "mcp:capture"
  ],
  "resource_documentation": "https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization"
}