# Member integration signature spec

> The fields of the member integration user_data string, how to build the signature (hash), the time sync checks, and fixed values for testing.

In member integration, `user_data` is a string your server signs from member info using the chat room token. This page is the authoritative reference for the fields, the signing order, and the rules the server checks. For the setup flow and examples in each language, see [Member integration](/en/doc/2.1).

## Fields {#fields}

Only the fields below **that have a value** are signed. They are signed in field name order, so the order you add them in does not matter. The signing code signs every field you add, so adding a field missing from this table causes error 202. If fields are added later, the signature of a site that does not send the new fields stays the same.

| Field | Format | Description |
| --- | --- | --- |
| `auth` | `0`–`3` or `guest`, `member`, `subadmin`, `admin` | Permission. `0` guest, `1` member, `2` moderator, `3` admin. If empty: member when `id` is set, otherwise guest |
| `icons` | List of URLs | Icons next to the nickname. Separate several with commas; those before `@` go before the nickname, those after go after it. Example: `/img/a.png,/img/b.png@/img/c.png` |
| `id` | String | Member ID. The basis for bans, mutes, and granted permissions |
| `level` | Number | Level. Used for level comparisons in chat room feature conditions |
| `nick` | String | Nickname. The server cuts it to 15 characters, then removes whitespace |
| `nickcon` | URL | Image shown instead of the nickname |
| `other` | String | Extra display. Separate words with a single space. Usually left empty |
| `room` | Chat room ID | Required. Must match the `<u-chat room>` value |
| `time` | Unix time (seconds) | Required. The current time when you create the signature. Not checked when time sync is off, but always include it |

The signing key is the chat room token. Find it in the admin page under **Chat room info → Token**. Do not put it in the resulting string. It is a different value from the API key for the server REST API. If the token leaks, anyone can join as an admin, so keep it only in your server code.

## Signing order {#algorithm}

1. Pick the fields that have a value. Empty and missing fields are left out, and so is the token.
2. Sort the fields by name (lowercase ASCII, byte order).
3. Make one line `name=value` per field, where the value is its UTF-8 bytes written as lowercase hexadecimal.
4. Join `uchat-v2` and the lines from step 3, separated by a single newline (`\n`) each.
5. Compute HMAC-SHA256 of the step 4 string with the chat room token as the key. The `hash` is the 64 lowercase hex characters prefixed with `v2.`.
6. In the same order, join `name + space + encodeURIComponent(value)` with `|`, and append `hash value` at the end.

Result shape:

```text
icons ...|id ...|level ...|nick ...|room my-room|time 1700000000|hash v2.0123...(64 chars)
```

- Encode values like JavaScript `encodeURIComponent`. A space becomes `%20`. In PHP, use `rawurlencode`. If you encode extra characters such as `~`, `*`, and `!` (as `rawurlencode` does), they decode to the same value, so the hash is the same.
- The chat window decodes each value with `decodeURIComponent` and sends it to the server. The server recomputes the hash in the same order and compares. The hex in `hash` is case-insensitive.
- HMAC-SHA256 and hex conversion are in the standard library of most languages (PHP 5.6+ `hash_hmac`·`bin2hex`, Node.js `crypto`, Python `hmac`·`hashlib`, Java `javax.crypto.Mac`, C# `HMACSHA256`, Go `crypto/hmac`, Ruby `OpenSSL::HMAC`).

Reference implementation (Node.js):

```js
const crypto = require('node:crypto');

function signUserData(user, token) {
	const fields = Object.entries({ ...user, time: Math.floor(Date.now() / 1000) })
		.filter(([, v]) => v !== undefined && v !== null && v !== '')
		.map(([k, v]) => [k, String(v)])
		.sort(([a], [b]) => (a < b ? -1 : 1));
	const message = ['uchat-v2', ...fields.map(([k, v]) => k + '=' + Buffer.from(v, 'utf8').toString('hex'))].join('\n');
	const hash = crypto.createHmac('sha256', token).update(message, 'utf8').digest('hex');
	return fields
		.map(([k, v]) => k + ' ' + encodeURIComponent(v))
		.concat('hash v2.' + hash)
		.join('|');
}

// signUserData({ room: 'my-room', nick: '홍길동', id: 'user1', level: 5 }, process.env.UCHAT_TOKEN)
```

## Fixed values for testing {#test-vector}

Use the input below to check your own code. You should get the same result.

| Input | Value |
| --- | --- |
| Token | `test-token-1234` |
| `time` | `1700000000` |
| `room` | `my-room` |
| `nick` | `홍 길동~*!` |
| `id` | `user@example.com` |
| `level` | `5` |
| `auth` | (empty) |
| `icons` | `/img/a.png,/img/b.png@/img/c.png` |

The string fed to HMAC (step 4; the empty `auth` has no line):

```text
uchat-v2
icons=2f696d672f612e706e672c2f696d672f622e706e67402f696d672f632e706e67
id=75736572406578616d706c652e636f6d
level=35
nick=ed998d20eab8b8eb8f997e2a21
room=6d792d726f6f6d
time=31373030303030303030
```

Resulting `user_data`:

```text
icons %2Fimg%2Fa.png%2C%2Fimg%2Fb.png%40%2Fimg%2Fc.png|id user%40example.com|level 5|nick %ED%99%8D%20%EA%B8%B8%EB%8F%99~*!|room my-room|time 1700000000|hash v2.80d95cf251e7aeef90b37547dbe95ef5336f6c7f8cb235a6cf6cee91031e2ae6
```

`auth` is empty, so it is left out of both `user_data` and the signature. If you build it with PHP `rawurlencode`, `~`, `*`, and `!` become `%7E`, `%2A`, and `%21`. The text differs, but the `hash` is the same and it works as is.

## Server checks {#checks}

In a chat room with member integration on, the chat server checks the following, in order, when someone joins.

| Check | Error on failure |
| --- | --- |
| Is there a `hash`? | 203 |
| Is the hash correct (recomputed in the order above)? | 202 |
| If time sync is on: is `time` not older than the reference, and has less than 300 seconds passed since the same signature was first used? | 205 |

The signature has no one-time value (nonce). With time sync off, anyone can copy a `user_data` and reuse it at any time, so keep time sync on.

## Time sync {#time-sync}

`time` is checked only when you turn on **Time sync** on the **Member integration** screen of the admin page. When it is off, `time` is only part of the signature and its value is not checked.

- The server calculates `offset = time - current time on the uchat server`.
- If no reference is stored yet, it stores this `offset` as the reference (first join).
- If a reference exists and `offset` is **more than 300 seconds smaller** than the reference (stale join data), it rejects with error 205. Future times are not blocked.
- The chat server accepts the same `hash` for only 300 seconds after it first receives it. After that, you get error 205.
- The reference moves to the new `offset` only when a join passes with a `hash` the server has not seen before. Reusing the same `user_data` does not move it.
- Turning on **Fixed standard time** sets the reference to 0. A `time` more than 300 seconds behind the uchat server clock is rejected. Use this when your server clock is accurate.
- If you move your server or set its clock back by more than 300 seconds, reset the **Time reference** in the admin page.
- In a chat room with time sync on, a `time` that is not a number gives error 101.

If you cache `user_data` in the page, error 205 can start after 5 minutes. Don't cache pages with member integration, or fetch it fresh each time with `user_data_url` ([u-chat attributes](/en/doc/3.1)). For fixes by error, see [Member integration](/en/doc/2.1#troubleshooting).
