Verify Your Users
Make sure the user id your app sends really belongs to the signed-in user.
Verify Your Users
When your app calls identifyUser, the SDK tells Setgreet which user is signed in. By default Setgreet takes that user id as given. User verification lets Setgreet confirm that the id really belongs to the signed-in user before it reads or changes anything stored for them.
It works with a short value called a user hash. Your server computes it from the user id and a secret that only you and Setgreet know, and your app passes it to identifyUser next to the id. Setgreet checks the hash on every request. An id without a valid hash is never matched to the stored profile of the user it names.
When to turn it on
Turn on user verification when any of these apply:
- Your flows show stored user data, for example through variable bindings like
{{user.firstName}}. - You target flows or branch Condition nodes on user attributes.
- Your user ids are easy to guess, such as email addresses or sequential database ids.
- You use External Data with signed requests and your API returns data for the user named in the request.
If your flows only use anonymous or generic content, you can leave it off. Nothing changes for an app until you turn it on.
How it works
- You create a verification secret for your app in the dashboard.
- Your server computes the user hash from the user id with that secret, and returns it to your app with the user's session.
- Your app passes the hash to
identifyUser. The SDK stores it with the user id and sends it with every request for that user. - Setgreet checks the hash against the id. A request with a valid hash is treated as that user.
Users who are not signed in are not affected. Before identifyUser, the SDK uses an anonymous id and sends no hash, and anonymous users keep getting their flows.
Step 1: Create the secret
Open your app's settings in the dashboard and create a user verification secret. Admins and owners of the organization can create, view and rotate it.
The secret starts with sgiv_. You can view it again later from the same place, for example when you set up a new server environment.
Keep the secret on your server only. Store it like any other server credential (for example in an environment variable or a secrets manager). Never put it in your app, in client-side code, or in a repository. Anyone who has the secret can produce a valid hash for any user id.
Step 2: Compute the hash on your server
The user hash is the HMAC-SHA256 of the user id, keyed with your secret, written as lowercase hex:
userHash = hex(HMAC-SHA256(key = secret, message = userId))- The key is the whole secret, including the
sgiv_prefix, as UTF-8. - The message is the user id exactly as your app passes it to
identifyUser, as UTF-8 text. A numeric id42is hashed as the text"42". - The result is 64 hex characters.
Compute it wherever your server already knows who the user is, such as your login or session endpoint, and return it to the app along with the user's id.
import crypto from 'node:crypto';
export function setgreetUserHash(userId) {
return crypto.createHmac('sha256', process.env.SETGREET_IDENTITY_SECRET).update(String(userId)).digest('hex');
}import hashlib
import hmac
import os
def setgreet_user_hash(user_id):
secret = os.environ["SETGREET_IDENTITY_SECRET"]
return hmac.new(
secret.encode("utf-8"),
str(user_id).encode("utf-8"),
hashlib.sha256,
).hexdigest()require "openssl"
def setgreet_user_hash(user_id)
OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("SETGREET_IDENTITY_SECRET"), user_id.to_s)
endpackage setgreet
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"os"
)
func UserHash(userID string) string {
mac := hmac.New(sha256.New, []byte(os.Getenv("SETGREET_IDENTITY_SECRET")))
mac.Write([]byte(userID))
return hex.EncodeToString(mac.Sum(nil))
}<?php
function setgreet_user_hash(string $userId): string
{
return hash_hmac('sha256', $userId, getenv('SETGREET_IDENTITY_SECRET'));
}Check your implementation
Run your function with this test secret and compare the output. The test secret is sgiv_ followed by 64 zeros:
sgiv_0000000000000000000000000000000000000000000000000000000000000000| User id | Expected hash |
|---|---|
alice@example.com | 6f84015e20dbf8596a439744883acc9938b319af85471537cb7047c66e922954 |
42 | 5e8582e80c872dfdb3269d436f5819f8dc689019576c690b20280272a3cd01c1 |
If your output differs, check that you used the whole secret as the key (prefix included) and the user id as the message, not the other way around.
Step 3: Pass the hash to identifyUser
Update the Setgreet SDK in your app to a version that includes the userHash parameter, then pass the hash from your server as the last argument of identifyUser. Everything else about the call stays the same (see User Identification).
// iOS
Setgreet.shared.identifyUser(
userId: "user_123",
attributes: ["plan": "pro"],
userHash: userHashFromYourServer
)// Android
Setgreet.identifyUser(
userId = "user_123",
attributes = mapOf("plan" to "pro"),
userHash = userHashFromYourServer
)// React Native
import { identifyUser } from '@setgreet/react-native-sdk';
identifyUser('user_123', { plan: 'pro' }, 'create', undefined, userHashFromYourServer);// Flutter
await Setgreet.identifyUser(
'user_123',
attributes: {'plan': 'pro'},
userHash: userHashFromYourServer,
);The SDK keeps the hash with the user id and sends it on every request for that user. It forgets the hash when you call resetUser or identify a different user id, so always pass the hash that belongs to the id in the same call.
The hash belongs to one exact user id. If your app identifies a user with a different id than the one your server
hashed (for example a different letter case or a prefix), verification fails. Hash the same value you pass as
userId.
Step 4: Roll it out
User verification has three modes, set per app in your app's settings:
| Mode | What happens |
|---|---|
| Off | No checks. This is the default. |
| Monitor | Every hash is checked and the results are shown in the dashboard. Nothing your users see changes. |
| Enforce | A user id without a valid hash cannot read or change the data stored for that user. Identify is refused. |
Monitor and Enforce need a secret. Turning the mode back to Off keeps the secret, so you can turn it on again later without changing your server.
Off to Monitor
Once your server returns the hash and an app build that passes it is out, switch to Monitor. Your app keeps working exactly as before, and the dashboard starts reporting what it sees:
- Last verified: the last time a request arrived with a valid hash. This tells you your server and your new app build work together. It can only move when a correct hash is sent, so it is the signal to rely on.
- Last invalid hash: the last time a hash arrived that did not match the user id. This usually means the hash was computed for a different id, or with an old secret.
- Last identify without a hash: the last time an
identifyUsercall arrived without a valid hash. Under Enforce, that call would be refused. While users are still on app versions that do not pass the hash, expect this to keep moving.
The last two are advisory. Your app key ships inside your app, so anyone who has it can make these two move. Use them as hints while you roll out, not as proof.
While the mode is Monitor or Enforce, Setgreet's responses to SDK requests that name a user carry an X-Setgreet-Identity header, with the value verified, unverified or invalid. It is a quick way to check a single request while you test.
Monitor to Enforce
Switch to Enforce when Last verified is recent and Last identify without a hash has stopped moving, which means most of your users are on an app version that passes the hash.
Under Enforce:
identifyUserwithout a valid hash is refused. Nothing is stored for that user from that call.- Users on app versions without the hash still see flows. Their sessions are treated as unverified: flows are shown, but their stored attributes stop updating and personalized content (bindings, attribute targeting, Condition nodes) behaves as for a user with no stored data, until they update the app.
- Anonymous users are not affected. They keep getting their flows, display history and experiment assignment. When they sign in with a valid hash, their anonymous session is merged into their profile as usual.
A few things to expect when you switch:
- An anonymous user's earlier display history is not carried over, so a flow set to show only once can show once more to anonymous users.
- Unverified sessions appear in the dashboard's end-user views and in your integrations with the id prefixed by
sg-unverified:. Do not use user ids that start withsg-unverified:yourself. - A user whose session is counted as unverified for part of a billing period can be counted as two monthly active users for that period. Expect a small bump in the period you switch.
- Turning Enforce off again sends anonymous users back to their original ids. Data written while unverified stays where it is.
Rotating the secret
Rotate the secret from your app's settings if you think it is known outside your team, or as a regular practice.
When you rotate, a new secret becomes current and the old one keeps working for a grace window: 24 hours by default, adjustable from 0 to 168 hours (7 days). Hashes made with either secret are accepted during the window, so your users are not interrupted.
- Rotate in the dashboard and copy the new secret.
- Deploy the new secret to your server within the grace window.
- Once your server uses the new secret everywhere, the old secret expires at the end of the window. You can also end the window early from the dashboard.
Only one previous secret is kept. Rotating again during a grace window retires the older secret immediately. If the old secret must stop working right away, set the grace window to 0 or end it after deploying the new one.
Rotating changes the hash of every user. Hashes your app stored from the old secret stop verifying when the window ends, so make sure your app gets a fresh hash from your server, for example on the next session refresh or app launch.
External Data with signed requests
If you use External Data with signed requests, your API receives the end user id in the X-Setgreet-End-User-Id header. The request signature proves the call came from Setgreet, but it does not cover which user the app named.
- App set to Enforce: the header is sent only for verified users, so you can trust it as the signed-in user. Requests for unverified sessions are still signed but arrive without
X-Setgreet-End-User-Id. Answer them as you would an anonymous visitor. - App set to Off or Monitor: the header carries the id the app sent, which is not verified. Do not use it alone to decide which user's personal data to return.
When a flow binds a signed Data Source on an app that does not enforce user verification, the dashboard shows a warning at publish.
Next steps
- User Identification -- the full
identifyUserAPI, attributes and reset. - External Data -- signed requests and the headers your API receives.
- Variable Bindings -- showing stored user data in a flow.