05 · Token Verification and Security
Whether push or pull, the developer ultimately obtains a wsa (an HS256 JWT). This article is required reading for every integrator — lax verification means the identity can be forged.
Mandatory Verification Checklist
After obtaining the wsa, verify it in order in the extension app's backend service (verifyWsa has all of this built in):
- Signature: verify with HS256 using the secret distributed/obtained at registration. On failure → reject.
exp: the current time ≤exp(including a small leeway).expmust be present — a token without anexpshould be rejected outright (otherwise it would never expire).iss: must equalgptbots-workspace.aud: must equal the target extension app's host. This is the key line of defense against "a token being stolen to attack another app," and it must not be omitted.iat/nbf(if present): must not be in the future (beyond leeway), to prevent a token whose "issued-at time is set far in the future" from being usable long-term.workspace_idtenant partitioning: if the extension implements multi-workspace isolation, attribute the request to thatworkspace_idand forbid cross-tenant access.- Strip the
wsa/codefrom the URL immediately after landing: usehistory.replaceStateto avoid the user sharing the token when they copy the URL. - Establish your own session: exchange it for the extension app's own session/cookie; subsequent requests must not depend on the
wsa— it expires in 5 minutes.
The official SDK's
verifyWsarequiresexpto be present, verifies future drift ofiat/nbf, and performs constant-time signature comparison and an algorithm allowlist (by default onlyHS256, eliminatingalgconfusion attacks). Using it automatically satisfies items 1–5.
Recommended: Verify with the SDK
import { verifyWsa, WsaVerificationError } from '@gptbots/workspace-extension-verify';
try {
const id = verifyWsa(wsa, {
secret: process.env.EXTENSION_APP_SECRET,
audience: 'app.example.com',
});
// id: { accountId, role, workspaceId, username?, email?, avatar?, appName?, issuedAt?, expiresAt? }
} catch (e) {
if (e instanceof WsaVerificationError) {
// e.code: InvalidToken | InvalidSignature | Expired | NotYetValid
// | WrongIssuer | WrongAudience | MissingClaim | UnsupportedAlgorithm
}
}
For the full options and error codes, see SDK API Reference.
Verification Without the SDK (Multi-Language)
The wsa is a standard HS256 JWT, and any JWT library can verify it. Be sure to explicitly enable iss / aud verification and lock algorithms=['HS256'].
Java (auth0 java-jwt)
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SHARED_SECRET))
.withIssuer("gptbots-workspace")
.withAudience("app.example.com")
.acceptLeeway(30) // tolerate 30s of clock drift
.build();
DecodedJWT jwt = verifier.verify(wsaParam);
String userId = jwt.getSubject();
String workspaceId = jwt.getClaim("workspace_id").asString();
String role = jwt.getClaim("role").asString();
String username = jwt.getClaim("username").asString(); // may be null
String email = jwt.getClaim("email").asString(); // may be null
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
const payload = jwt.verify(wsaParam, SHARED_SECRET, {
algorithms: ['HS256'],
issuer: 'gptbots-workspace',
audience: 'app.example.com',
clockTolerance: 30,
});
const { sub: userId, workspace_id, role, username, email, avatar } = payload;
Python (PyJWT)
import jwt
payload = jwt.decode(
wsa_param,
SHARED_SECRET,
algorithms=["HS256"],
issuer="gptbots-workspace",
audience="app.example.com",
leeway=30,
)
user_id = payload["sub"]
workspace_id = payload["workspace_id"]
role = payload["role"]
Secret encoding reminder: HS256 uses the secret string's UTF-8 bytes directly as the HMAC key. The Tier 2 per-app secret looks like
wext_+64 hex; pass it in as the raw string as the key (do not hex-decode it again), consistent with the platform's signing side.
Security Requirements (Implement Each One)
- The secret is the entire security model. The current HS256 is a symmetric key: once leaked, anyone can forge any workspace user's identity to attack the integrating extension app. Keep it only on the backend (environment variables/KMS) — never put it in frontend code, git repositories, logs, or client configuration.
- The JWT's exposure surface in the URL (push only): the query string is captured by browser history, web server access logs, CDN cache logs, and Referer records. Even if it is logged, it can still be exploited if obtained within 5 minutes. Immediately
history.replaceStateto strip it after landing (consumeHandoffdoes this by default). Pull (M-Auth) inherently never puts thewsain the URL, making it more secure. - Do not pass the
wsathrough to sub-resources. After exchanging it for your own session, subsequent XHR/fetch/img must never carry the originalwsa, otherwise it would appear in the Referer of every sub-resource. - Clock synchronization. HS256 judges
expstrictly, so the server clock must be NTP-synced; the 30sleewayin the example tolerates minor drift — do not expand it to the minute level. roleis only a default permission mapping. It only reflects the user's role in this workspace, not the permissions inside your app; have your app maintain its own permission model.- Multi-tenant isolation. Always use
workspace_idas the data isolation key to prevent a user in workspace A from reading workspace B's data. - Secret rotation/revocation emergency: in Space Management, "rotate the secret" for the app, which generates a new secret and displays it one time; then reconfigure your backend with the new secret.
5. The Frontend Trust Boundary (Important)
- The frontend does display placeholders only, not authorization decisions. The frontend can base64-decode the JWT payload, but it cannot verify the signature without the secret — anyone can forge a payload that "looks correct."
- All decisions about "who this person is, and whether they can do something" must be based on the result of the extension app backend's
verifyWsa. - Therefore the standard strategy is: the frontend hands the
wsa/codeto the extension app backend → the backend verifies → the backend establishes a session → the frontend recognizes only this session.
Next step: SDK API Reference for the complete API; when you run into any problem, consult Troubleshooting.
