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:
| Environment | URL |
|---|---|
| Production | https://id.icp.invent.us/.well-known/openid-configuration/jwks |
| Development | https://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 theemailclaim is safe to use as a verified identity attribute.role(andprofile, 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 bytenantto prevent cross-tenant data leakage.iss— issuer URL. Should always behttps://id.icp.invent.us(prod) orhttps://id.dev.icp.invent.us(dev) — your JWT library can verify this for you by passing the expectedissuer.exp/iat/nbf— expiry, issued-at, not-before. Your JWT library checks these automatically. Every freshhttpClientcall 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
issuertohttps://id.icp.invent.us(prod) orhttps://id.dev.icp.invent.us(dev). Rejects tokens signed by some other RS256 IdP. - Pins
audiencetoapi(or whatever audience your endpoint expects). Without this, any valid Invent token — including ones issued for unrelated services — would pass. - Scopes every query by
tenantif 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 rejectalg: noneandHS256— both well-known JWT bypass tricks. Without pinning, well-crafted attacker tokens can defeat verification entirely. - Never logs the raw
Authorizationheader or the full decoded payload. Both contain PII (name, email, employee number, roles, sub) and a live token that's replayable untilexp. If you need debug logging, logsubandtenantonly. - 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.verifydoes 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
ProxyAppcomponent. - 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.