Access Tokens & Verification
How to generate tokens, use them in requests, and verify incoming tokens in your application.
Generating a Token#
Before you can generate a token, you need a registered app and an active access grant. If you haven't done this yet, see the Registering Your App guide.
To generate a token:
- Go to Dashboard → Applications → your app
- Find the grant in the Outbound tab
- Click "Generate Token"
- Copy the JWT immediately — it's shown only once
Tokens are shown only once
Using a Token in Requests#
Send the JWT in the Authorization header as a Bearer token:
GET /api/orders HTTP/1.1
Host: inventory.internal.jlstradingco.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...In code, this looks like:
const response = await fetch("https://inventory.internal.jlstradingco.com/api/orders", {
headers: {
"Authorization": `Bearer ${YOUR_TOKEN}`,
},
});What's Inside the Token#
Each JWT contains these claims. You don't need to decode the token yourself (the verification library does this), but it helps to understand what's in there:
{
"iss": "https://eds.jlstradingco.com",
"sub": "a1b2c3d4-...",
"aud": "e5f6g7h8-...",
"app_name": "Scheduling Tool",
"iat": 1713200000,
"exp": 1744800000
}| Name | Type | Required | Description |
|---|---|---|---|
iss | string | Yes | Issuer — always the EDS base URL. Verifying this confirms the token came from your EDS deployment. |
sub | string (UUID) | Yes | Subject — the unique ID of the app that is making the request (the calling app). |
aud | string (UUID) | Yes | Audience — the unique ID of the app this token is intended for (your app, if you're verifying). Always check this matches your app ID. |
app_name | string | Yes | Human-readable name of the calling app. Useful for logging and audit trails, but don't use it for authorization decisions. |
iat | number | Yes | Issued-at timestamp (Unix seconds). When the token was created. |
exp | number | Yes | Expiration timestamp (Unix seconds). Tokens expire 1 year after issuance by default. |
Verifying Tokens in Your App#
If other apps are calling your app with tokens, you need to verify those tokens are genuine and intended for you. This is done with the jose library and the EDS public key.
Step 1: Install the jose library
npm install joseStep 2: Create a verification helper
Create this once and reuse it across your routes:
// lib/verify-eds-token.ts
import { createRemoteJWKSet, jwtVerify } from "jose";
// This fetches the public key from EDS and caches it automatically.
// It only re-fetches when it encounters an unknown key ID (during key rotation).
const JWKS = createRemoteJWKSet(
new URL("/.well-known/jwks.json", process.env.EDS_BASE_URL || "https://eds.jlstradingco.com")
);
// Your app's ID from the App Registry (found in your app's detail page URL)
const MY_APP_ID = process.env.EDS_APP_ID!;
export interface VerifiedApp {
appId: string; // The calling app's ID (sub claim)
appName: string; // The calling app's human-readable name
}
export async function verifyAppToken(token: string): Promise<VerifiedApp> {
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.EDS_BASE_URL || "https://eds.jlstradingco.com",
audience: MY_APP_ID,
});
return {
appId: payload.sub!,
appName: payload.app_name as string,
};
}Step 3: Use it in your routes
Here's how to use the helper in a Next.js API route, an Express route, or any HTTP handler:
// Example: Next.js API route
import { NextRequest, NextResponse } from "next/server";
import { verifyAppToken } from "@/lib/verify-eds-token";
export async function GET(request: NextRequest) {
// 1. Extract the token from the Authorization header
const authHeader = request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json(
{ error: "Missing or invalid Authorization header" },
{ status: 401 }
);
}
// 2. Verify the token
try {
const callingApp = await verifyAppToken(authHeader.slice(7));
console.log(`Request from: ${callingApp.appName} (${callingApp.appId})`);
// 3. Your app decides what this caller can do
// The token only proves identity — authorization is up to you.
return NextResponse.json({ data: "your response here" });
} catch (err) {
// Token is invalid, expired, or not meant for this app
return NextResponse.json(
{ error: "Invalid or expired token" },
{ status: 401 }
);
}
}// Example: Express middleware
import { verifyAppToken } from "./lib/verify-eds-token";
async function requireAppAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing token" });
}
try {
req.callingApp = await verifyAppToken(authHeader.slice(7));
next();
} catch {
return res.status(401).json({ error: "Invalid token" });
}
}
// Use it:
app.get("/api/orders", requireAppAuth, (req, res) => {
console.log(`Called by: ${req.callingApp.appName}`);
res.json({ orders: [] });
});What Verification Checks#
When you call jwtVerify, it automatically checks:
- Signature — the token was actually signed by EDS (not forged or tampered with)
- Expiration— the token hasn't expired
- Issuer — the token was issued by your EDS deployment (not some other system)
- Audience — the token was meant for your app (not a different app)
If any of these checks fail, jwtVerifythrows an error. You don't need to check these manually.
Authorization Is Up to You#
The token proves who is calling — it does not define what they can do. After verifying the token, your app decides how to handle the request. Some examples:
- *Allow any verified app — if a token is valid and addressed to you, let the request through. Simplest approach, good for read-only APIs.
- *Allowlist specific apps — check the
sub(calling app ID) against a list of approved app IDs. Good for sensitive operations. - *Different permissions per app — use the
subto look up what the calling app is allowed to do. Most flexible, requires maintaining a permissions map.
Token Lifecycle#
How long do tokens last?
Tokens expire 1 year after issuance. This is intentionally long because these are service-to-service credentials, not user sessions.
Can I regenerate a token?
Yes. Click "Generate Token" on the same grant again. The old token remains valid until it expires — generating a new token does not invalidate old ones.
What happens when a grant is revoked?
No new tokens can be generated for that grant. However, tokens already issued remain valid until they expire. This is a known trade-off of local verification — there's no central server to invalidate tokens in real time.
What if I lose a token?
Generate a new one. There's no way to retrieve a previously displayed token.
JWKS Endpoint Reference#
Public key for token verification — no authentication required
Best Practices#
- *Always verify the audience claim. Pass your app ID as the
audienceoption tojwtVerify. This prevents a token meant for App X from being accepted by App Y. The code examples above do this automatically. - *Log the calling app. After verification, log
sub(app ID) andapp_name. This creates an audit trail of which apps are calling yours and when. - *Return clear error messages. When verification fails, return a 401 with a message like "Invalid or expired token" so the calling app knows to regenerate. Avoid leaking internal details.
- *Store tokens securely. Use environment variables or a secrets manager for the tokens you send. Never commit tokens to source code or share them in chat.
- *Set up your app ID as an environment variable. Store your registered app ID as
EDS_APP_IDin your environment. This keeps your verification code portable across environments (dev, staging, production).
Troubleshooting#
"JWSSignatureVerificationFailed"
The token's signature doesn't match the public key. This usually means the token was issued by a different EDS instance than the one you're verifying against. Check that your EDS_BASE_URL points to the correct EDS deployment.
"JWTExpired"
The token has passed its expiration date. The calling app needs to generate a new token from their grant in the dashboard.
"JWTClaimValidationFailed" with audience mismatch
The token was meant for a different app. The calling app generated a token targeting the wrong app ID. They need to create a grant targeting your app and generate a new token.
JWKS fetch fails
The createRemoteJWKSetfunction can't reach the JWKS endpoint. Verify your app can access the EDS host. After the initial fetch, keys are cached, so this only affects the first request or key rotation events.