Skip to main content

Verifying Invent JWTs with JWKS

If you're a vendor building a backend that receives API calls from your Invent micro-app widget, you don't need to set up OAuth to authenticate those calls. Every request a widget makes through the platform-provided httpClient carries a signed JWT issued by Invent — your server can verify that token directly against our public JWKS endpoint.

The user is already authenticated to the Invent portal when they open a widget. The platform mints a JWT for them automatically and httpClient attaches it to every outbound request — you don't run any login flow yourself.

How it works at a glance​

When a widget calls your backend via httpClient, the Invent platform attaches a signed Bearer token to the request:

Authorization: Bearer <invent-jwt>

Your server verifies that token by fetching Invent's public signing keys from our IdP's JWKS endpoint:

EnvironmentURL
Productionhttps://id.icp.invent.us/.well-known/openid-configuration/jwks
Developmenthttps://id.dev.icp.invent.us/.well-known/openid-configuration/jwks

Both URLs are publicly reachable and serve the same JWKS for all portals in that environment — you don't need a per-tenant URL.

Because the keys are public, you can verify Invent's signatures without holding any shared secret with us. If verification succeeds, you can trust the decoded payload as identity — then proceed to your own authorization checks (see Security checklist below). If verification fails, return 401 / 403.

That's the whole authentication story for backends that only need to confirm "this request really came from a real Invent user." No OAuth flow, no token exchange, no callbacks.

What's in the token​

A decoded payload looks like this:

{
"iss": "https://id.icp.invent.us",
"aud": "api",
"scope": ["openid", "api.default"],
"amr": ["external"],
"client_id": "po-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"sub": "01953da0-6c13-70c4-9921-8d0e34293d79",
"tenant": "tttttttt-tttt-tttt-tttt-tttttttttttt",
"portal": "pppppppp-pppp-pppp-pppp-pppppppppppp",
"idp": "iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii",
"name": "Jane Advisor",
"email": "jane@example.com",
"employee_number": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
"role": ["advisor", "licensedassociate", "dashboardeditor"],
"profile": ["advisor", "licensedassociate", "dashboardeditor"],
"groups": ["gggggggg-gggg-gggg-gggg-gggggggggggg"],
"nbf": 1779828721,
"iat": 1779828721,
"exp": 1779832321,
"auth_time": 1779818449,
"sid": "...",
"jti": "..."
}

The claims you'll typically use:

  • sub — stable user UUID. The user's identity.
  • name / email — display name and email. Invent users can't self-sign-up — accounts are created and emails set by Invent — so the email claim is safe to use as a verified identity attribute.
  • role (and profile, usually identical) — array of role strings (e.g. advisor, dashboardeditor).
  • tenant / portal — UUIDs identifying which Invent tenant and portal the user is currently on. Required if you serve multiple Invent tenants — scope every query by tenant to prevent cross-tenant data leakage.
  • iss — issuer URL. Should always be https://id.icp.invent.us (prod) or https://id.dev.icp.invent.us (dev) — your JWT library can verify this for you by passing the expected issuer.
  • exp / iat / nbf — expiry, issued-at, not-before. Your JWT library checks these automatically. Every fresh httpClient call carries a current token, so you don't need to refresh anything yourself.

Minimal example (Node / Express)​

The smallest viable backend that accepts a request from a widget, verifies the Invent JWT, and uses the decoded user info:

const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const jwks = require('jwks-rsa');

// One constant for the IdP — change this single line to switch environments.
// Dev: 'https://id.dev.icp.invent.us'
// Prod: 'https://id.icp.invent.us'
const INVENT_IDP = 'https://id.icp.invent.us';

const client = jwks({
jwksUri: `${INVENT_IDP}/.well-known/openid-configuration/jwks`,
});

const getKey = (header, cb) =>
client.getSigningKey(header.kid, (err, key) => cb(err, key?.getPublicKey()));

const app = express();

// Allow the Invent portal origin — widgets call you from there in the browser.
// For multiple portals (dev / staging / prod), pass an array: origin: ['https://a...', 'https://b...']
// No `credentials: true` — httpClient sends the JWT in the Authorization header, not a cookie.
app.use(cors({ origin: 'https://your-portal.apptrium.cloud' }));

app.get('/data', (req, res) => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) return res.sendStatus(401);
const token = auth.slice(7);

jwt.verify(
token,
getKey,
{
algorithms: ['RS256'], // pin algorithm — see Security checklist
issuer: INVENT_IDP, // pin issuer to Invent IdP
audience: 'api', // pin audience — reject tokens not for our API
clockTolerance: 5, // seconds; tolerate small clock skew
},
(err, user) => {
if (err) return res.sendStatus(403);
// Multi-tenant: always scope queries by user.tenant before returning data.
res.json({ hello: user.name });
},
);
});

app.listen(3000);

Install: npm install express cors jsonwebtoken jwks-rsa. The jwks-rsa client caches keys, so you're not hitting the JWKS endpoint on every request. For other languages, any standard JWKS-aware JWT library works the same way.

CORS: the widget runs in the user's browser on the Invent portal domain, so its requests to your server are cross-origin. Set the origin to the exact portal URL your widget loads from (e.g. https://integrations-dev.apptrium.cloud).

Security checklist​

Signature verification alone is not enough. Make sure your server also:

  • Pins issuer to https://id.icp.invent.us (prod) or https://id.dev.icp.invent.us (dev). Rejects tokens signed by some other RS256 IdP.
  • Pins audience to api (or whatever audience your endpoint expects). Without this, any valid Invent token — including ones issued for unrelated services — would pass.
  • Scopes every query by tenant if you serve multiple Invent tenants. The signature only confirms who the user is; it doesn't authorize them to read another tenant's data.
  • Pins algorithms: ['RS256'] when verifying. This is what makes the library reject alg: none and HS256 — both well-known JWT bypass tricks. Without pinning, well-crafted attacker tokens can defeat verification entirely.
  • Never logs the raw Authorization header or the full decoded payload. Both contain PII (name, email, employee number, roles, sub) and a live token that's replayable until exp. If you need debug logging, log sub and tenant only.
  • Serves over HTTPS in production, both for your endpoints and to fetch the JWKS endpoint. TLS is what guarantees you reach Invent's real IdP rather than a hijacked DNS response. A JWT captured in transit can be replayed until its exp.
  • Allows a small clockTolerance (a few seconds) so minor clock skew between your server and Invent's IdP doesn't reject valid tokens.
  • Remembers JWTs are stateless. A disabled user's token remains valid until exp (up to ~1 hour). For high-stakes operations, consider additional checks against your own user-state store.
  • Rate-limits unauthenticated and failed-auth requests. jwt.verify does crypto work on every request — an attacker spraying invalid tokens can sustain CPU load. Cap requests per IP / per token so a stream of bogus tokens can't degrade your endpoint.

When you do still need OAuth​

JWKS verification is enough for backends that respond to live widget requests from users currently signed in to an Invent portal. You'll need something beyond JWKS in these cases:

  • Your backend calls a third-party API (Salesforce, Google, any SaaS your widget integrates with). You need OAuth for that third party — not for Invent. Use the ProxyLogin tutorial and the platform's ProxyApp component.
  • Background jobs that act on behalf of a user. The 1-hour Invent JWT only exists while the user is actively using the widget — there's no offline / refresh token. For scheduled work, store whatever you need at the time of the request (e.g. a per-tenant API key) rather than relying on the JWT.
  • System-to-system calls with no user session. JWKS verifies user JWTs; service accounts need a different mechanism — either an Invent-issued machine credential (if available) or your own internal auth between services.