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.