logo
Development
Search
05 · Token Verification and Security

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):

  1. Signature: verify with HS256 using the secret distributed/obtained at registration. On failure → reject.
  2. exp: the current time ≤ exp (including a small leeway). exp must be present — a token without an exp should be rejected outright (otherwise it would never expire).
  3. iss: must equal gptbots-workspace.
  4. 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.
  5. 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.
  6. workspace_id tenant partitioning: if the extension implements multi-workspace isolation, attribute the request to that workspace_id and forbid cross-tenant access.
  7. Strip the wsa / code from the URL immediately after landing: use history.replaceState to avoid the user sharing the token when they copy the URL.
  8. 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 verifyWsa requires exp to be present, verifies future drift of iat/nbf, and performs constant-time signature comparison and an algorithm allowlist (by default only HS256, eliminating alg confusion attacks). Using it automatically satisfies items 1–5.


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 } }
                      
                      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
  }
}

                    
This code block in the floating window

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
                      
                      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

                    
This code block in the floating window

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;
                      
                      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;

                    
This code block in the floating window

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"]
                      
                      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"]

                    
This code block in the floating window

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)

  1. 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.
  2. 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.replaceState to strip it after landing (consumeHandoff does this by default). Pull (M-Auth) inherently never puts the wsa in the URL, making it more secure.
  3. Do not pass the wsa through to sub-resources. After exchanging it for your own session, subsequent XHR/fetch/img must never carry the original wsa, otherwise it would appear in the Referer of every sub-resource.
  4. Clock synchronization. HS256 judges exp strictly, so the server clock must be NTP-synced; the 30s leeway in the example tolerates minor drift — do not expand it to the minute level.
  5. role is 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.
  6. Multi-tenant isolation. Always use workspace_id as the data isolation key to prevent a user in workspace A from reading workspace B's data.
  7. 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/code to 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.