Skip to main content

Secure User Identity Verification

Secure User Identity Verification lets an Admin require that Guides and Surveys accept a user's identity only when your server has signed it. Your server signs a short-lived JSON Web Token (JWT) with a secret key that an Admin generates in Fullstory, and your page passes that token to the Guides and Surveys script. When enforcement is on, guide progress and survey data for identified users are loaded and saved only for requests that carry a valid token.

This page covers the developer's side of the setup: the required token claims, signing samples, how to send and test a token, token lifetime, and troubleshooting. An Admin generates keys, activates enforcement, rotates keys, and turns the feature off in settings. For those steps, see Secure User Identity Verification in the Help Center.

How Verification Works​

Verification applies only to identified users. A request from the Guides and Surveys script counts as identified when it includes a user ID or a token:

  • If your page sets window.usetifulTags.userId (or calls USETIFUL.user.setId), that value is the user ID.
  • Otherwise, the user ID is the uid you pass to FS('setIdentity', ...).
  • Any request that includes a userToken is treated as identified.

A Fullstory session ID on its own is not an identity. Requests from visitors with no user ID and no token are allowed in every state, including enforcement.

How a request is handled depends on the state an Admin has set on the User Identity Verification settings card. Until a secret key is generated, the card shows a Set up Identity Verification button or a toggle that reads In setup. After that, the toggle reads In testing mode or On, where On means enforcement is on.

StateValid tokenInvalid or expired tokenUser ID, no tokenNo user ID and no token
User Identity Verification not set upToken is not checked.Token is not checked.Allowed.Allowed.
In testing modeAllowed.Rejected with 403.Allowed.Allowed.
Enforcement onAllowed.Rejected with 403.Rejected with 403.Allowed.

In testing mode and enforcement, a valid token's sub claim replaces any other user ID in the request, and progress is stored under sub. If a request is rejected, guides and surveys don't load or save for that user.

Use testing mode to roll out tokens safely. Invalid tokens are already rejected, but requests without a token are still allowed. Once every page with identified users sends a valid token, an Admin can activate enforcement.

Signing a User Token​

Sign the token on your server with the private key downloaded from settings. The key is an RSA private key in PKCS#8 PEM format (-----BEGIN PRIVATE KEY-----).

The token must meet all of the following requirements. Fullstory doesn't check iat or nbf.

  • Algorithm: The header alg must be RS256. Tokens signed with HS256 or none are rejected.
  • sub: Required. A string or an integer containing the user's ID. The samples below use a string. Use the ID the script would otherwise send: the value you would set as usetifulTags.userId, or, if you don't set one, the uid you pass to FS('setIdentity', ...). If you use a different value, progress is stored under a new ID and the user's existing guide progress starts over.
  • iss: Required. Your Fullstory org ID. You can find it in your Fullstory app URL (https://app.fullstory.com/ui/<orgId>/...) and as the value of window['_fs_org'] in your recording snippet.
  • exp: Required. The time the token expires, in seconds since the Unix epoch. The token is rejected as soon as exp passes, with no allowance for clock differences. There is no maximum lifetime, but we recommend a lifetime between a few minutes and 8 hours. Keep your server's clock synced with NTP. The samples below use 1 hour.

The following samples are the same ones shown on the settings card. Each takes the user ID, your org ID, and the private key, and returns a signed token.

// npm install jose
import { importPKCS8, SignJWT } from 'jose';

export async function signUserToken(
userId: string,
orgId: string,
privateKey: string,
): Promise<string> {
const key = await importPKCS8(privateKey, 'RS256');
return new SignJWT({})
.setProtectedHeader({ alg: 'RS256' })
.setSubject(userId)
.setIssuer(orgId)
.setExpirationTime('1h')
.sign(key);
}

// Usage: load the key once at the application boundary and pass it in.
const token = await signUserToken(userId, orgId, process.env.GUIDES_AND_SURVEYS_PRIVATE_KEY!);

Sending the Token​

Render the signed token into the page as window.usetifulTags.userToken before the Fullstory recording snippet (or the standalone Guides and Surveys snippet) loads:

<!-- Render this on your server, before the Fullstory recording snippet.
Replace the placeholder with the signed token returned by the backend example above. -->
<script>
window.usetifulTags = {
userToken: "<SIGNED_TOKEN_FROM_YOUR_BACKEND>"
};
</script>

Don't also set userId in window.usetifulTags when you send a token, because the token's sub is the user ID. In testing mode and enforcement, if a request includes both, a valid token's sub is used and userId is ignored.

The script reads window.usetifulTags.userToken every time it loads or saves progress. If you replace the value later on a long-lived page, the next request uses the new token.

Testing a Token​

While you develop, use Test Your Token to check a token. It's on the User Identity Verification card in Settings → Account Management → Guides and Surveys, and it stays disabled until your org has a secret key. The tester sends the token to Fullstory and tells you which requirement, if any, the token fails.

The tester applies the same rules as requests from your site. A token that passes the tester isn't rejected later because of its claims or timing, as long as you use it before exp. For a valid token, the tester can also show a warning without rejecting the token, when exp looks like milliseconds or is more than 24 hours away. For the exact messages, see Troubleshooting.

The tester checks only the token you paste. To confirm that your page sends the token and that your site's requests are accepted, use your browser's developer tools while your org is in testing mode:

  1. Open the Network tab and load a page as a signed-in user.
  2. Find the POST requests to /api/get and /api/save.
  3. Confirm that the request payload includes userToken.
  4. Confirm that the response status is 200. A 403 means the request was rejected.

A 403 response body is always one of the two values below. Neither names the claim that failed. To find out which claim failed, paste the token into Test Your Token.

Response bodyMeaning
{"error":"Invalid userToken"}The request included a token, and the token failed verification.
{"error":"userToken is required"}Enforcement is on, and the request included a user ID but no token.

Token Lifetime and Re-issuing​

Sign a new token each time your server renders a page for a signed-in user. There is no refresh endpoint. If a page can stay open longer than the token's lifetime, your app must fetch a new token from your server and assign it to window.usetifulTags.userToken before the old one expires.

Set exp in seconds, not milliseconds. A millisecond value such as Date.now() is accepted, but it is read as seconds, so the token effectively never expires. Test Your Token warns about this but doesn't reject the token.

If your server's clock runs behind, tokens expire earlier than you intended, and very short lifetimes can expire before the page uses them. Keep your server's clock synced with NTP and use a lifetime of at least a few minutes.

An individual token can't be revoked before its exp, and logging a user out doesn't invalidate a token they've already been issued. Keep lifetimes short for this reason. Deleting a secret key is how you stop accepting tokens signed with that key. The deletion normally takes effect immediately, but in rare cases a deleted key can still be accepted for up to 24 hours.

Keeping Keys and Tokens Secure​

  • Sign tokens only on your server. Never put the private key in browser code, a mobile app, or a public repository.
  • Store the private key in a secret manager and load it into your signing code from there. Fullstory stores only the public key and shows the private key once, when it's generated.
  • If a key is lost or exposed, rotate it: generate a new key, switch your server to it, then delete the old key. Keep signing with the previous key until requests signed with the new key are accepted. For the steps, see Rotate or replace a secret key in Secure User Identity Verification. Don't turn the feature off to replace a key. Turning it off deletes every key.

Troubleshooting​

When a token's signature or format fails, Test Your Token shows one of these messages:

  • The token is not a well-formed JWT. It must have three dot-separated parts.
  • The token header must use "alg": "RS256". Your token uses "<alg>". If the header has no alg, the second sentence is left out.
  • The signature does not match any of your active secret keys.

When the signature is valid but a claim fails, the tester shows The JWT signature is valid but the data inside is not. followed by one message for each claim that failed:

  • "sub" is required. Set it to the ID of the user the token is issued for.
  • "iss" is required and must be your organization ID: <orgId>.
  • "iss" must be your organization ID: <orgId>. Your token has "<received>".
  • "exp" is required and must be a time in the future, in seconds since the Unix epoch.
  • "exp" is in the past. The token has expired. Issue a new token.

When a token is valid, the tester can also show one of these warnings. A warning doesn't make the token invalid:

  • "exp" looks like milliseconds. It must be in seconds since the Unix epoch.
  • This token is valid for <duration> from now. We recommend at most 24 hours: a leaked token cannot be revoked before it expires. This warning appears when exp is more than 24 hours away. Keep lifetimes to 24 hours at most, and preferably 8 hours or less, as described in Signing a User Token.
ProblemCauseFix
403 Invalid userToken: token expiredexp has passed. A token is rejected as soon as the current time reaches exp, both in the tester and on your site.Sign a new token for each page render. If tokens expire earlier than expected, sync your server's clock with NTP and use a lifetime of at least a few minutes.
403 Invalid userToken: missing claimsub, iss, or exp is missing. sub must be a string or an integer.Set all three claims, as the samples do.
403 Invalid userToken: wrong issueriss doesn't match your Fullstory org ID.Use the org ID from your app URL or window['_fs_org'].
403 Invalid userToken: wrong algorithmThe token was signed with an algorithm other than RS256.Sign with RS256.
403 Invalid userToken: invalid signatureThe token was signed with a deleted key, another org's key, or the wrong file.Compare the key hint on the settings card (for example, MII****FsY=) with the beginning and end of the key body in your .pem file.
403 after switching to a newly generated keyA key change normally takes effect immediately. In rare cases, a new key can be rejected for up to 24 hours.Keep signing with the previous key until requests signed with the new key return 200. Likewise, a deleted key is normally rejected immediately, but in rare cases it can still be accepted for up to 24 hours.
403 userToken is requiredEnforcement is on, and the request included a user ID but no token.Send a token on every page where users are identified.
No userToken in the requestwindow.usetifulTags.userToken wasn't set before the snippet loaded on the first page load.Render window.usetifulTags before the snippet. Values you assign later are picked up by later requests.
Guides and surveys work in testing mode but stop after enforcementTesting mode allows requests without a token. Enforcement doesn't.Find the pages where users are identified but no token is sent.
A user's guide progress starts oversub doesn't match the ID the script used before, so progress is stored under a new ID.Set sub to the same ID you used for usetifulTags.userId or FS('setIdentity', ...).