Five Minute Auth

Sign users in with devices or your own accounts.

Product
Version

Introduction

This documentation refers to version 1.0 of Five Minute Player Authentication.

User authentication lets your client prove who the user is when it connects, instead of simply declaring an id. A signed-in user gets a stable unique user id that only they can use, a session that survives reconnects, and sign-in methods they can add or remove. Leaderboards, experiments, metric events and moderation all act on the unique user id, so signing users in is what makes those features trustworthy: without it, any client can claim any id.

This document is for developers of clients that sign users in, and for the developers of your own server, who need to know which unique user id to use and what the server can and cannot do about a user's identity.

Requirements

  • A Five Minutes client library on protocol 2.0: the npm packages 2.0 for a web page or another JavaScript client, which sign users in with their signIn option (see npm packages 2.0 and Client libraries in the Five Minute wire protocol).
  • The Unity asset cannot sign users in: it speaks protocol 1.7 or earlier (asset 1.20 speaks protocol 1.6, asset 1.23 speaks protocol 1.7). Its users keep self-declared ids, and it is refused by any application that turns on Require authenticated sessions. So are the npm packages 1.8.
  • Signing users in works on every plan. Requiring it for every login is a per-application setting that depends on your plan (see Requiring authenticated sessions).
  • For the Custom sign-in method: your own backend that verifies users (for example through Google, Apple, Steam or your own accounts).

Quick facts for AI agents

  • Covers: user sign-in on the wire (Device and Custom), the identity resolution order, sessions and tokens, logout, link and unlink, refusals and client retry logic, the Require authenticated sessions setting, and which unique user id your server uses.
  • Build with: the client signs in through a Five Minutes client library on protocol 2.0, never through its own implementation of the protocol: in JavaScript, the npm packages 2.0 with signIn, session, onSessionChanged and logout() (see npm packages 2.0); the Unity asset does not sign users in. Your server uses the Server API routes named below.
  • Your server: base URL https://signalr.fiveminutes.cloud/api/v1. Signing a user in needs no server call. Related routes: POST /api/v1/{applicationId}/user/register (scope Application.Users.Register for Standard users, or Application.Admin.FullAccess, "Full access") pre-creates a user that a later sign-in adopts, which is how your server chooses a user's unique user id (see Knowing which user a client is); minting ephemeral app keys (Application.ClientCredentials.Mint) and banning users (Application.Admin.FullAccess) are documented in the Server API.
  • Server auth: POST /api/v1/api-clients/authenticate with {"applicationId":"<application id>","clientSecret":"<API key secret>"} returns {"token","expiresInSeconds":600,"scopes"}; send Authorization: Bearer <token>.
  • Client: protocol 2.0; messages ClientUserInfoResponse (sign-in fields), ServerWelcome, ClientLogoutRequest, ClientLinkIdentityRequest, ClientUnlinkIdentityRequest and their typed responses; send only after ServerWelcome; a new MessageId per message; match replies on ResponseToMessageId. (ClientUserInfoResponse is the login step itself; ServerWelcome answers it.)
    • Resolution order: SessionToken > RefreshToken > AuthProvider > legacy. The first non-empty field wins; a failure never falls through to the next one.
    • Refusal: ServerGoodbye with ReasonType UserAuthenticationFailed and AllowAutoReconnect false. A server error or an outage during the sign-in, or a sign-in the server could not decide yet, is no refusal: ReasonType InternalServerError and AllowAutoReconnect true. Keep the stored tokens and the credential.
    • Access token lifetime 3600 seconds; refresh token 30 days, sliding, single use. New tokens arrive only on refresh and credential logins.
    • ServerWelcome.UniqueUserId is authoritative on every login. Always adopt it.
    • AuthProvider is exactly Device or Custom (case-sensitive).
  • Identifiers: unique user id (UniqueUserId): 64 lowercase hex characters for a user created by signing in, or the adopted legacy id (at most 256 characters). It is private to the user: never show it to other users or send it to them; they address the user by DisplayId. Refresh token: 64 lowercase hex characters. Access token: a JWT; treat it as opaque.
  • Limits: UniqueUserId 1-256 characters; Username 3-64 letters, digits or -; AuthDeviceId at most 256 characters; AuthCustomId at most 256; AuthSecret at most 1024.
  • Retries: never repeat a refused input automatically; after a refused access token or refresh token, log in once with the next input. After an InternalServerError goodbye, log in again with the same input and the same UniqueUserId after a bounded backoff, at most 5 times; the same credential signs in the same user. Every attempt uses a new connection, new MessageIds and, if you use them, a new ephemeral app key. Repeating a logout, or a link to the same user, is harmless.
  • Does not exist: an in-session token refresh message; a REST endpoint to verify an access token, look up the user behind a CustomId, list or revoke sessions, or list identities; a public JWKS; a message that lists a user's sign-in methods; account merge; Custom secret rotation; a warn-only mode for Require authenticated sessions; any AuthProvider other than Device and Custom.
  • Related: Five Minute wire protocol, Five Minute Server API, Five Minute API Contracts.

Concepts

Application admission and user identity. Every connection passes two separate checks. Application admission is the handshake step where the client sends ClientCredentialsResponse with your Application ID and an application secret or ephemeral app key; it proves the connection belongs to your application and says nothing about who the user is. User identity is the next step, ClientUserInfoResponse, which decides which user the connection acts as for the rest of its life. This document covers the second step. The handshake itself is described in the Five Minute wire protocol.

Application ID. The GUID that identifies your application.

Application secret. The static secret the client sends in the handshake (dashboard: "App client secrets"). It admits the application only and is never used for REST calls.

Ephemeral app key. An ecs_... string your server mints for one connection and hands to the client, which sends it in place of the application secret. It admits the application only and carries no user identity.

User. One account in one application. Users never span applications: the same device or the same CustomId in two applications gives two unrelated users.

Unique user id. The id every other feature uses for the user: UniqueUserId on the wire, uniqueUserId in REST, ownerId for a user's leaderboard record. A user created by signing in gets an id of 64 lowercase hex characters. A legacy user, or a legacy user adopted by a sign-in, keeps the id the client declared, for example user-42. The unique user id is private to the user: never show it to other users or send it to them, and never put it into anything other users receive, such as the user's name or metadata, chat, whisper or leaderboard metadata, or a channel name. Other users see and address the user by display id. Until your application requires authenticated sessions, anyone who knows a unique user id can log in as that user (see Legacy login).

Display id. DisplayId: the public id other users see and use to address the user. It is not a secret. It is generated from the user's name when the user is created (the first six characters of the name, padded with x, then seven digits, for example Alicex4821937) and generated again when the name is changed with ClientSetUsernameRequest or the Server API. A rename through Username or AuthUsername at login keeps the display id. Leaderboard entries show other users by display id and name (OwnerDisplayId, OwnerName), never by unique user id.

Legacy login. A login that presents no token and no credential. The server trusts the UniqueUserId the client declares (a self-declared id), so anyone who knows or guesses the id can act as that user. Every client below protocol 2.0, including every version of the Unity asset, logs in this way.

Authenticated login. A login that presents a SessionToken, a RefreshToken or an AuthProvider credential, and so proves the user's identity.

Sign-in method. A credential attached to a user, also called an identity. There are two providers, Device and Custom. A user can hold several of each.

Device. Sign-in with a device id alone. There is no secret: whoever has the device id is the user.

Custom. Sign-in with a CustomId and Secret pair that your own backend issues after it has verified the user.

Access token. A short-lived token (3600 seconds) returned in ServerWelcome.AccessToken. The client presents it as SessionToken when it reconnects.

Refresh token. A long-lived token returned in ServerWelcome.RefreshToken. It is valid for 30 days, and every use replaces it with a new one that is valid for another 30 days. The client presents it as RefreshToken to get a new token pair when the access token has expired. Each refresh token works once.

Session. One sign-in of one user, typically on one device. Every credential login starts a new session; a refresh continues it with a new token pair; logout, revocation or 30 days without a refresh end it. An access token and a refresh token always belong to one session and one application.

Require authenticated sessions. A per-application setting. When it is on, a login that presents no token and no credential is refused, and a user can no longer name another user by that user's unique user id.

How a login decides who the user is

The server reads the sign-in fields of ClientUserInfoResponse in a fixed order and takes the first branch whose field is non-empty (a value of only spaces counts as empty). A failure in the chosen branch refuses the login; the server never falls back to a later branch. An outage is no such failure: when a service the branch needs does not answer, the login ends with a goodbye that allows a reconnect, and nothing about the user is decided (see When a login is refused).

Branch Taken when Server checks On failure ServerWelcome returns
SessionToken SessionToken is non-empty. Always validated, on every application, even when Require authenticated sessions is off. The access token is genuine, not expired, issued for this application, and its session has not been revoked. ServerGoodbye UserAuthenticationFailed. The refresh token and the credential are not tried. AccessToken and RefreshToken empty, ExpiresIn 0, UniqueUserId = the token's user, which overrides the UniqueUserId you sent. No new tokens: keep your stored pair.
RefreshToken SessionToken is empty and RefreshToken is non-empty. The refresh token is the current one of its session (not already used), not revoked, not expired, issued for this application, and the user is not banned. ServerGoodbye UserAuthenticationFailed. The credential is not tried. A new, rotated AccessToken and RefreshToken, ExpiresIn 3600, UniqueUserId.
AuthProvider Both token fields are empty and AuthProvider is non-empty. The provider name, the credential fields, the credential itself (or AuthCreate for an unknown one), and that the user is not banned. ServerGoodbye UserAuthenticationFailed. A new AccessToken and RefreshToken, ExpiresIn 3600, UniqueUserId = the existing user, the adopted legacy id, or a newly created id. The UniqueUserId you sent is only an adoption hint.
Nothing (legacy) SessionToken, RefreshToken and AuthProvider are all empty. Credential fields sent without AuthProvider are ignored. Only whether the application requires authenticated sessions. ServerGoodbye UserAuthenticationFailed when the setting is on. AccessToken and RefreshToken empty, ExpiresIn 0, UniqueUserId = the id you sent.

After the branch has decided who the user is, the server completes the login as that user. On the SessionToken and legacy branches a banned user is refused at this point, with InvalidCredentials; on the RefreshToken and AuthProvider branches the ban is already caught inside the branch, with UserAuthenticationFailed. A non-empty Username renames the user on every branch (see User names).

Rules that follow from the order:

  • Present one input per login: a token or a credential. You may send SessionToken and RefreshToken together, but only the SessionToken is used. A valid one leaves the refresh token unused; an expired one refuses the login even though the refresh token would have worked.
  • UniqueUserId must be set on every login (the contracts library refuses to send an empty one). On the token branches it is ignored; on the AuthProvider branch it is offered for one-shot adoption; only on the legacy branch is it trusted.
  • ServerWelcome.UniqueUserId is set on every login of a protocol 2.0 client, including a legacy login. Store it and use it as the unique user id everywhere, including in your server's calls.
  • Replace your stored token pair only when ServerWelcome.AccessToken is non-empty.

Example: first sign-in with a device

A fresh install signs in with its device id. No legacy user has the id it sends, so a new user is created.

ClientUserInfoResponse:

Field Value
UniqueUserId <new GUID> (any non-empty value; see Keeping existing users)
Username empty
SessionToken empty
RefreshToken empty
AuthProvider Device
AuthDeviceId <device id>
AuthCustomId empty
AuthSecret empty
AuthCreate true
AuthUsername Alice

ServerWelcome:

Field Value
DisplayId Alicex4821937
AccessToken <access token> (a JWT)
RefreshToken <refresh token>
ExpiresIn 3600
UniqueUserId <unique user id>

The client stores the pair, the expiry (receipt time + 3600 seconds) and the unique user id.

Example: signing in through your backend

Your backend has verified the user and returned a CustomId and Secret. Your app used to log this user in as the legacy id user-42, and keeps sending that id so the existing user is adopted.

ClientUserInfoResponse:

Field Value
UniqueUserId user-42 (the id your app used before)
Username empty
SessionToken empty
RefreshToken empty
AuthProvider Custom
AuthDeviceId empty
AuthCustomId <custom id>
AuthSecret <secret>
AuthCreate true
AuthUsername empty (ignored for Custom)

ServerWelcome:

Field Value
DisplayId the display id user-42 already had
AccessToken <access token>
RefreshToken <refresh token>
ExpiresIn 3600
UniqueUserId user-42 (adopted)

Had user-42 not existed, or had another sign-in already adopted it, UniqueUserId would be a new 64-character hex id instead.

Example: reconnecting with a valid access token

The client reconnects 20 minutes later. Its access token has 40 minutes left, so it sends only the access token.

ClientUserInfoResponse:

Field Value
UniqueUserId <unique user id> (the stored id)
SessionToken <access token>
RefreshToken empty
AuthProvider empty
AuthDeviceId, AuthCustomId, AuthSecret, AuthUsername empty
AuthCreate true (the library default; ignored on this branch)

ServerWelcome:

Field Value
AccessToken empty
RefreshToken empty
ExpiresIn 0
UniqueUserId <unique user id>

The client keeps its stored pair and its stored expiry.

Example: reconnecting after the access token expired

The client reconnects the next day. The access token has expired, so it sends only the refresh token. Sending the expired access token as well would refuse the login.

ClientUserInfoResponse:

Field Value
UniqueUserId <unique user id>
SessionToken empty
RefreshToken <refresh token>
AuthProvider empty
AuthCreate true (ignored on this branch)

ServerWelcome:

Field Value
AccessToken <new access token>
RefreshToken <new refresh token> (the old one no longer works)
ExpiresIn 3600
UniqueUserId <unique user id>

The client replaces its stored pair and expiry at once.

Signing in with a device (Device)

The Device method signs a user in with a device id alone. It is the right choice for guest access and for apps without accounts.

Set these fields on ClientUserInfoResponse and leave both token fields empty:

Field Value
AuthProvider Device (exactly this spelling)
AuthDeviceId your stable per-install id, at most 256 characters
AuthCreate true to create the user the first time this device is seen; false to refuse unknown devices
AuthUsername optional name for the user; see below
UniqueUserId the stored unique user id, or the old legacy id on the first sign-in (see Keeping existing users)

Rules:

  • The device id is a bearer credential. There is no secret, so anyone who has the device id is the user. Never show it, log it or send it anywhere else.
  • A device id belongs to one application. The same device id in two applications gives two unrelated users.
  • The contracts library sets AuthCreate to true unless you set it to false; the server applies no default of its own. With false, an unknown device is refused with "Player authentication failed: credential rejected."
  • AuthUsername names the new user when the device is first seen, and renames the existing user on every later Device sign-in that carries it. A non-empty Username in the same message wins, because it is applied afterwards. Leave AuthUsername empty unless you want to set the name.
  • Use a stable id per install: generate a random GUID on first launch and persist it (on WebGL, in browser storage). Reinstalling, or clearing the storage, loses the id; the next Device sign-in with AuthCreate true then creates a new user. Link a Custom sign-in method if users must be able to recover their account on another device.
  • Five Minutes never stores the raw device id. The dashboard shows a keyed hash of it instead.

With the npm packages 2.0, pass the credential as signIn. The client fills AuthProvider, AuthDeviceId and AuthUsername from it and AuthCreate from create (default true), and refuses a device id that is empty or longer than 256 characters, and a name that breaks the rules, before it connects. createDeviceId() makes the per-install id, also on pages served over http:

import { ChatClient, createDeviceId } from "@fiveminutes-io/chat-client";

// Once per install: a new device id is a new user
const deviceId = localStorage.getItem("fm-device") ?? createDeviceId();
localStorage.setItem("fm-device", deviceId);

const client = new ChatClient({
  serverUrl: "https://signalr.fiveminutes.cloud",
  applicationId: "<application id>",
  applicationSecret: "<application secret>",
  signIn: { provider: "Device", deviceId, username: "Alice" },
  uniqueUserId: legacyId,     // the id your app used before, on the first sign-in; see Keeping existing users
  session: storedSession,     // the stored session, see Sessions and tokens
});
await client.connect();

Signing in through your backend (Custom)

The Custom method signs a user in with a CustomId and Secret pair that your own backend issues after it has verified the user with any system you choose. Use it for accounts, cross-device access and platform logins.

  1. Client: signs the user in with your system (Google, Apple, Steam, your own accounts or anything else) and calls your backend with the proof that system gives it.
  2. Your server: verifies the proof and gets a stable subject for the user (for example the platform's user id).
  3. Your server: returns a CustomId and a Secret for that subject to the client, over your own authenticated connection.
  4. Client: connects to Five Minutes and sends ClientUserInfoResponse with AuthProvider = Custom, AuthCustomId, AuthSecret, AuthCreate and UniqueUserId, both token fields empty. Success is a ServerWelcome with a non-empty AccessToken.

Five Minutes never calls your backend. It only checks that the Secret matches the one stored when the CustomId was first used.

Derive both values on your server from the verified subject with a key only your server holds, so the same subject always gets the same pair and you never have to store them:

key      = a random 32-byte key stored only on your server (never in the client)
subject  = "<provider>:<verified subject>"          for example "steam:<Steam id>"

CustomId = "c_" + lowercase_hex( HMAC_SHA256(key, "customid|" + subject) )  -> 66 characters
Secret   = base64url( HMAC_SHA256(key, "secret|" + subject) )                 -> 43 characters, no padding

Rules:

  • Never use a third-party token (an OAuth access token, an ID token, a session ticket) as the Secret. Those change, and the stored secret never does: the user would be locked out at the next sign-in.
  • The first sign-in with a new CustomId and AuthCreate true creates the user and stores that secret. Creation is first come, first served, so a CustomId must not be guessable: someone who guesses it before the real user signs in owns it. HMAC-derived ids are not guessable.
  • Use one letter case for CustomId (the example uses lowercase hex) and never issue two ids that differ only in case.
  • A wrong secret and an unknown CustomId with AuthCreate false give the same refusal, "Player authentication failed: credential rejected.", so the response never reveals whether an id exists.
  • There is no secret rotation: the secret stored for a CustomId never changes on its own. Linking the same CustomId to the same user again with another secret succeeds but keeps the original secret. Derive the secret deterministically so it never has to change. (The only way to store a new secret is to unlink Custom and link it again; see Troubleshooting.)
  • The contracts library defaults AuthCreate to true, so a Custom sign-in with an unknown CustomId creates a user unless you set it to false. Use false when a sign-in must never create a user, for example on a "sign in to your existing account" screen.
  • AuthUsername is ignored for Custom. To name the user, use Username.
  • Keep the pair on the device in secure storage, or fetch it from your backend whenever the client needs a credential login.

With the npm packages 2.0, pass a function as signIn to fetch the pair from your backend only when the client needs a credential: on the first sign-in, and when it holds no usable token. Add create: false to the returned credential where a sign-in must never create a user. The client refuses an empty customId or secret, a customId longer than 256 characters and a secret longer than 1,024 before it connects:

import { ChatClient } from "@fiveminutes-io/chat-client";

const client = new ChatClient({
  serverUrl: "https://signalr.fiveminutes.cloud",
  applicationId: "<application id>",
  applicationSecret: "<application secret>",
  signIn: async () => {
    const pair = await (await fetch("/api/fiveminutes-pair")).json();   // your backend, after verifying the user
    return { provider: "Custom", customId: pair.customId, secret: pair.secret };
  },
  session: storedSession,
});

User names

  • Username: 3-64 characters, each a letter, a digit or -. The contracts library refuses to send a message with an invalid non-empty Username. The server refuses one too: a login with it ends with a RequestFormatViolation goodbye (see When a login is refused), and a ClientSetUsernameRequest with it gets Success false and the rule's text.
  • A non-empty Username renames the user on every login, whatever the branch. Send it empty to keep the current name.
  • A user created with no name gets Anonymous- followed by 7 digits, for example Anonymous-4821937.
  • AuthUsername applies to Device sign-ins only (see Signing in with a device). The contracts library does not check it, so follow the Username rules yourself.
  • The npm packages 2.0 check both before connecting: their username and signIn.username options must be empty or follow the rules, or connect() rejects with a ClientConfigError. An e-mail address is not a valid name; it can be the unique user id.
  • The display id is derived from the name the user had when the user was created. A user created by a Custom sign-in, or by a Device sign-in without AuthUsername, starts as Anonymous-..., so their display id starts with Anonym, and a Username in the same message renames them without changing the display id. Use AuthUsername on the first Device sign-in if the display id should follow the name.
  • To rename a user during a session, send ClientSetUsernameRequest (see Five Minute API Contracts). It also gives the user a new display id; read it from ServerSetUsernameResponse.DisplayId.

Keeping existing users

Users who logged in with a self-declared id before you added sign-in keep their account, with its display id, name, chat history, scores and moderation state, through one-shot adoption (a Username, or a Device AuthUsername, in the same message still renames it). The first authenticated sign-in takes over the old id when all four conditions hold:

  1. It is a credential login (the AuthProvider branch) that creates a new user: the device or CustomId is not known and AuthCreate is true.
  2. Its UniqueUserId field is non-empty and at most 256 characters.
  3. A user with exactly that id already exists in the application, created by a legacy login or by your server's register call.
  4. No authenticated user has taken over that id before. Adoption happens once per id, ever.

When condition 1 holds but condition 2, 3 or 4 fails, the sign-in creates a new user with a new 64-character hex id instead, and the old user is left unchanged. (When condition 1 fails, a known credential logs in as the user it already belongs to, and an unknown one with AuthCreate false is refused.) Users are never merged afterwards, so get this right in the first release that signs users in.

Rules:

  • Adoption happens only on the credential login that creates the user. A login with a session token or refresh token, a login with a credential that already belongs to a user, and a link never adopt.
  • Keep sending the old id in UniqueUserId on the first authenticated sign-in. The Unity asset used the "Unique user id" field of its connection component (UserId in code), or SystemInfo.deviceUniqueIdentifier when that field was empty; send the same value. The npm packages 1.8 used their deviceId option. With the npm packages 2.0, pass the old id as uniqueUserId together with signIn: the client sends it in UniqueUserId until the server has named the user.
  • A banned legacy user cannot be adopted: the sign-in is refused and nothing is saved.
  • If the server cannot confirm in time whether the old user exists, it saves nothing and ends the login with an InternalServerError goodbye that allows a reconnect. Log in again with the same credential and the same UniqueUserId after a backoff; the adoption offer still stands.
  • If two new sign-ins offer the same id at the same moment, one adopts it and the other becomes a new user.
  • Adoption trusts the id the client sends, so a legacy id is only as private as it always was. While Require authenticated sessions is off, a legacy login that declares the adopted id still acts as that user; turn on Require authenticated sessions once your users have moved to signed-in clients.

Sessions and tokens

Lifetimes and storage

Item Lifetime What the client stores
Access token 3600 seconds from issue (ServerWelcome.ExpiresIn) The token and its expiry: receipt time + ExpiresIn seconds.
Refresh token 30 days from issue; every refresh replaces it and starts a new 30 days The token, in secure storage.
Unique user id Permanent ServerWelcome.UniqueUserId from the latest login.
Device id As long as the install keeps it The id, generated once per install.
Custom pair Permanent (the secret never changes) The pair in secure storage, or nothing if you fetch it from your backend when needed.

Store tokens and the unique user id per Application ID: a token works only for the application that issued it. Keep tokens in the platform's secure storage where there is one, never write them to logs, and treat the refresh token like a password: whoever holds it can sign in as the user for up to 30 days.

The npm packages 2.0 keep the unique user id, the token pair and the expiry for you. Store the session that onSessionChanged gives you (or getSession() returns) and pass it back as session when you create the client; null means forget it:

client.onSessionChanged((session) =>
  session ? localStorage.setItem("fm-session", JSON.stringify(session)) : localStorage.removeItem("fm-session"));

The server accepts an access token for up to 1 minute past its expiry, to allow for clock differences. Do not rely on this: send an access token only while your own clock says it is valid, and use the refresh token after that.

Reconnecting

Choose the input for each new connection from what you have stored:

  1. If you hold an access token and the current time is before its expiry minus a safety margin (60 seconds is a good margin), send it as SessionToken and leave RefreshToken empty.
  2. Otherwise, if you hold a refresh token, send it as RefreshToken and leave SessionToken empty.
  3. Otherwise, sign in with the credential (Device or Custom).

The npm packages 2.0 choose the input this way on every connection, reconnects included, with a margin of 60 seconds.

Rules:

  • A login with SessionToken returns no new tokens (ExpiresIn is 0) and does not extend the access token. Keep the expiry you computed when the token was issued.
  • A valid SessionToken sent together with a RefreshToken leaves the refresh token unused. An expired one refuses the login, even though the refresh token would have worked, so never send an access token you believe has expired.
  • Compute the expiry from ExpiresIn and your own clock at receipt. Do not parse the access token.

Refresh tokens rotate

  • A refresh token works once. A successful RefreshToken login returns a new pair, and the old refresh token stops working at that moment.
  • Of two logins that use the same refresh token at the same time, one gets the new pair and the other is refused with UserAuthenticationFailed. Keep one pair per install and never share it between processes or devices.
  • A refresh checks bans again: a banned user's refresh is refused. When the server cannot make that check because a service it needs does not answer, it exchanges nothing: the login ends with InternalServerError and AllowAutoReconnect true, and the same refresh token works on the next login.
  • If the connection drops after the server has rotated the token but before ServerWelcome reaches the client, the client still holds the old refresh token, which no longer works. Its next RefreshToken login is refused: drop the stored pair and sign in with the credential. A client must therefore always be able to fall back to its Device id or Custom pair.
  • A session that goes 30 days without a refresh expires. The next login needs the credential.

When the access token expires while connected

  • The connection stays up, and other messages keep working as the same user. The server checks the access token only when a connection logs in.
  • Link and unlink need a current access token on the connection. Once the access token from the login has expired they answer NotAuthenticated with "No authenticated player session is bound to this connection." After a SessionToken login, that is the presented token's own expiry, which may be only minutes away.
  • There is no message that refreshes tokens inside a connection. To get a fresh pair, reconnect with the refresh token, then send the link or unlink request. With the npm packages, call disconnect() and then connect(): once the access token has expired, the client logs in with the refresh token.

Several devices and regions

  • Every credential login starts a new, independent session with its own token pair. Signing in on a second device does not affect the first one. Signing in with the credential again on the same device also starts a new session and leaves the old one to expire, so reconnect with tokens whenever you have them.
  • Sessions work in every region. A token is accepted by every region of the same application, and rotation and logout take effect everywhere.

Revocation

A session is revoked when the user logs out, or when a member of your organization signs the user out on the dashboard.

  • The session's refresh token stops working at once, in every region.
  • The session's access token is refused on new logins once the revocation has spread: usually within seconds, allow up to 2 minutes. If the revocation cannot be distributed, the access token is refused only once it has expired: at most 3600 seconds after it was issued, plus the 1-minute clock allowance (see Lifetimes and storage).
  • Connections that are already logged in are not disconnected. On such a connection, link and unlink answer NotAuthenticated with "The player session bound to this connection is no longer valid."
  • Revocation is not a ban. The user can sign in again at once with any of their sign-in methods. To keep a user out, ban them (see Your server and user identity).

Signing out (ClientLogoutRequest)

The client ends its session with ClientLogoutRequest, sent after ServerWelcome. The server sends ServerAck as soon as it has the request, then answers with ServerLogoutResponse. Field tables: End the session (ClientLogoutRequest).

  1. Client: sends ClientLogoutRequest with RefreshToken set to its stored refresh token.
  2. Five Minutes: revokes the session that the refresh token belongs to and answers ServerLogoutResponse. Success = true means the session has ended.
  3. Client: deletes its stored access token and refresh token, then closes the connection itself.

Rules:

  • The whole session is revoked: its refresh token and every access token issued in it (see Revocation). The user's other sessions, for example on other devices, are not affected. No message signs a user out everywhere; the dashboard can.
  • Success is true also when the session was already revoked or has expired, so repeating a logout is harmless.
  • The server does not close the connection and sends no ServerGoodbye. From the response on, the connection holds no session (link and unlink answer NotAuthenticated), but it still acts as the same user for other messages until it closes. Disconnect right after the response.
  • Delete the stored tokens even when Success is false. Keep the device id, and the Custom pair if you store one, only if the user should be able to sign in again without your backend.
Success FailureReason (exact text) Meaning Retry?
true empty The session has ended, now or earlier. No need.
false The refresh token is not valid or has already been revoked. The refresh token is unknown, is an older token of a session that has since been refreshed, or belongs to another application. A current token of a revoked session gets Success = true instead. No. If you hold a newer refresh token of the same session, send that one.
false Logout could not be completed: the authentication service is unavailable. Temporary failure; the session may or may not have ended. Yes, after a short wait, with a new MessageId. A repeat is harmless.
false ApplicationId and RefreshToken are required. The request carried an empty refresh token. The contracts library refuses to send one. No. Send the stored refresh token.
false Logout failed (<status>). Unexpected answer. Once, later. Otherwise drop the tokens; the session expires 30 days after its last refresh.

With the npm packages 2.0, logout() sends ClientLogoutRequest with the stored refresh token and acts on the answer: on Success = true, and on The refresh token is not valid or has already been revoked., it forgets the tokens (onSessionChanged gets null) and disconnects. On Logout could not be completed: the authentication service is unavailable., and when no answer comes, it rejects and keeps the tokens: call it again. On any other refusal it forgets the tokens and disconnects, then rejects with the RequestFailedError. Without a refresh token, after a legacy login, it only forgets the user and disconnects.

try {
  await client.logout();
} catch (error) {
  // The session may not have ended: offer to try again
}

Linking and unlinking sign-in methods

A signed-in user can add sign-in methods and remove them. The usual case is a guest who started with Device and later creates an account: link the account as Custom, and the user can then sign in with either, on any device.

Link and unlink need a session on the connection: the connection logged in with a SessionToken, a RefreshToken or a credential, has not logged out, and the access token from that login has not expired. A connection that logged in the legacy way always gets NotAuthenticated.

Link a sign-in method (ClientLinkIdentityRequest)

Adds a Device or Custom sign-in method to the user on this connection. Reply: ServerAck, then ServerLinkIdentityResponse. Field tables: Add a sign-in method (ClientLinkIdentityRequest).

Turning a guest into an account holder:

  1. Client: is logged in on a Device sign-in (it holds a session).
  2. Client and your server: the user signs in to your system, and your server returns a CustomId and Secret (see Signing in through your backend).
  3. Client: sends ClientLinkIdentityRequest with Provider = Custom, CustomId and Secret.
  4. Five Minutes: answers ServerLinkIdentityResponse with Success = true and an empty ErrorCode. From now on a Custom sign-in with that pair, on any device, logs in as this user with the same UniqueUserId.
  5. If ErrorCode is IdentityAlreadyLinked, that account already belongs to another user, for example one created by signing in with Custom on another device first. Users are never merged. Let the user choose: stay on the guest user, or sign in with Custom and continue as the account's user (the guest user stays reachable only through its device id).

Unlink a sign-in method (ClientUnlinkIdentityRequest)

Removes every sign-in method of one provider from the user on this connection. Reply: ServerAck, then ServerUnlinkIdentityResponse. Field tables: Remove a sign-in method (ClientUnlinkIdentityRequest).

Rules for link and unlink:

  • A user can hold several Device and several Custom sign-in methods.
  • Linking a credential that belongs to another user fails with IdentityAlreadyLinked and changes nothing. Users are never merged.
  • Linking a credential that already belongs to this user succeeds and changes nothing. For Custom, the stored secret stays the original one.
  • Unlink removes all of the user's sign-in methods of that provider: every linked device, or every Custom id.
  • Unlink fails with LastIdentity when it would leave the user with no sign-in method, and with NotLinked when the user has none of that provider.
  • Neither link nor unlink ends sessions. A device whose Device method was unlinked stays logged in for as long as it holds a working refresh token; its next Device sign-in with AuthCreate true creates a new user.
  • Send Provider exactly as Device or Custom. The contracts library refuses any other value. The server compares link and unlink provider names without regard to letter case, but sign-in compares AuthProvider exactly, so always use the exact spelling.
  • Linking never adopts a legacy id.
  • No message lists a user's sign-in methods. Track what the user has linked in your app, or look at the user's page on the dashboard.
ErrorCode FailureReason (exact text) Returned by Meaning Retry?
NotAuthenticated No authenticated player session is bound to this connection. link, unlink The connection logged in the legacy way, logged out, or its access token has expired. Reconnect with the refresh token (or the credential), then send the request again.
NotAuthenticated The player session bound to this connection is no longer valid. link, unlink The session was revoked (a logout with this session's refresh token, or the dashboard) or its token is no longer accepted. Reconnect. The refresh token is probably revoked as well; then sign in with the credential.
IdentityAlreadyLinked The credential is already linked to a different player. link The device or CustomId belongs to another user. No.
LastIdentity The player's last identity cannot be unlinked. unlink Unlinking would leave the user without a sign-in method. No. Link another method first.
UnknownProvider The provider is not supported. link, unlink Provider is not Device or Custom in any letter case. An empty Provider is not recognised as a provider at all and answers ServiceUnavailable instead. No. Fix the request.
NotLinked The provider is not linked to this player. unlink The user has no sign-in method of that provider. No.
InvalidRequest the server's text, for example The provider's credential fields are required. link, unlink The server rejected the request. For link, the text above means DeviceId, or CustomId and Secret, are missing. No. Fix the request.
ServiceUnavailable The identity could not be linked: the authentication service is unavailable., The identity could not be unlinked: the authentication service is unavailable. or The authentication service answered <status>. link, unlink Temporary failure. Yes, with bounded backoff and a new MessageId each time. If the first attempt did go through, a repeated link answers success and a repeated unlink answers NotLinked.

On success, Success is true and ErrorCode and FailureReason are empty.

With the npm packages 2.0, send link and unlink with request. It resolves on Success = true and rejects with a RequestFailedError otherwise; its response holds the ErrorCode. isSignedIn() tells whether the connection holds a session. The client checks these requests before sending them: one with another provider name, or without its credential fields, rejects at once with a MessageValidationError and is not sent (see Checks before sending).

import { RequestFailedError } from "@fiveminutes-io/chat-client";
import type { ServerLinkIdentityResponse } from "@fiveminutes-io/contracts";

try {
  await client.request("ClientLinkIdentityRequest", { Provider: "Custom", CustomId: customId, Secret: secret });
} catch (error) {
  if (error instanceof RequestFailedError) {
    const code = (error.response as ServerLinkIdentityResponse).ErrorCode;   // for example "IdentityAlreadyLinked"
  }
}

When a login is refused

A refused login ends with a ServerGoodbye after the client has sent ClientUserInfoResponse. AllowAutoReconnect is always false for these refusals, and the server closes the connection shortly after the goodbye, so every further attempt needs a new connection. A server error during the login, the sign-in included, and a sign-in the server could not decide yet are no refusal: the server decided nothing about the credential, and the login ends with ReasonType InternalServerError and AllowAutoReconnect true (the last row of the table); log in again with the same input and the same UniqueUserId after a backoff. An outage is no refusal either: while a service the sign-in needs does not answer, the server saves nothing and exchanges no refresh token, and ends the login the same way, so keep the stored tokens and the credential. The same credential signs in the same user, also while the first attempt is still running on the server. A RefreshToken that login presented may already have been exchanged when the server failed after exchanging it, so the next login can refuse it; then go on with the credential, as for any refused refresh token. The full catalogue of goodbyes is in the Five Minute wire protocol.

ReasonType Reason (exact text) Meaning Retry?
UserAuthenticationFailed Your session could not be authenticated. Please sign in again. The SessionToken was refused (expired, revoked or issued for another application); or the RefreshToken was refused (already used, revoked, expired, issued for another application, or the user is banned); or the login presented nothing while Require authenticated sessions is on. After a SessionToken: reconnect once with the refresh token. After a RefreshToken: drop the stored pair and reconnect once with the credential. After a login that presented nothing: sign in with a credential (a client below protocol 2.0 cannot).
UserAuthenticationFailed Player authentication failed: credential rejected. A wrong Custom secret; an unknown device or CustomId with AuthCreate false; or a banned user. No automatic retry. Let the user try again.
UserAuthenticationFailed Player authentication failed: the user name is longer than 64 characters. A Device sign-in whose AuthUsername is longer than 64 characters. No. Fix the client.
UserAuthenticationFailed Player authentication failed: '<AuthProvider>' is not a supported provider or its credential fields are missing. AuthProvider is not exactly Device or Custom, or the fields it needs are empty. No. Fix the client.
UserAuthenticationFailed Player authentication failed: the issued session is not valid for this application. The new session was refused for this application. Rare. No automatic retry. Let the user try again later.
UserAuthenticationFailed any other text starting with Player authentication failed:, for example Player authentication failed: the credential could not be verified. The credential was refused for a reason the table does not list. No automatic retry.
InvalidCredentials Credentials could not be validated. At this step: the user is banned (on a SessionToken or legacy login). The same goodbye before ServerUserInfoRequest means the Application ID or application credential was refused instead. No.
RequestFormatViolation The text of the rule the message broke, for example Invalid username 'a b'. Username must conform to regular expression: ^([\p{L}\-\d]){3,64}$ ClientUserInfoResponse broke one of its rules, such as a Username that is not 3-64 letters, digits or -, or a UniqueUserId longer than 256 characters. The contracts library checks these before sending. No. Fix the client.
InternalServerError Internal Server Error: The user could not be signed in. Please contact support if this persists., or another text that starts with Internal Server Error: No refusal: the sign-in or the rest of the login failed on the server's side, for example because a service it needs did not answer or a token could not be checked; or the server could not decide the sign-in yet, typically whether an old user can be adopted or whether the user may sign in, and saved nothing. Nothing was decided about the credential, and a refresh token the server could not check was not exchanged. AllowAutoReconnect is true. Yes. Log in again with the same input and the same UniqueUserId after a bounded backoff; an adoption offer still stands. If the next login refuses the RefreshToken, go on with the credential.

A banned user is refused on every branch, with a different goodbye depending on what the client presented:

Login presented Goodbye a banned user gets
SessionToken InvalidCredentials, Credentials could not be validated.
Nothing (legacy) InvalidCredentials, Credentials could not be validated.
RefreshToken UserAuthenticationFailed, Your session could not be authenticated. Please sign in again.
AuthProvider UserAuthenticationFailed, Player authentication failed: credential rejected.

Recommended client behaviour

Present one input per login, fall back at most twice (access token, then refresh token, then credential), repeat an input only after a goodbye that allows a reconnect and only a bounded number of times, and always adopt the unique user id the server returns:

stored     : per Application ID { playerId, accessToken, accessTokenExpiresAt, refreshToken }
credential : Device(deviceId) or Custom(customId, secret)
legacyId   : the id your app used before it signed users in (empty for new installs)

choose_input():
    if stored.accessToken and now < stored.accessTokenExpiresAt - 60 seconds: return SESSION
    if stored.refreshToken: return REFRESH
    return CREDENTIAL

sign_in():
    input = choose_input()
    network_attempts = 0
    loop:
        open a new connection and complete the application step
            (with a new ephemeral app key if you use them)
        send ClientUserInfoResponse with a new MessageId and exactly one input:
            SESSION    : SessionToken = stored.accessToken
            REFRESH    : RefreshToken = stored.refreshToken
            CREDENTIAL : AuthProvider and its fields, AuthCreate
            UniqueUserId = stored.playerId, else legacyId, else a new random GUID
            Username     = a new name, or empty to keep the current one

        on ServerWelcome w:
            stored.playerId = w.UniqueUserId                      # always adopt
            if w.AccessToken is not empty:                        # replace the pair only then
                stored.accessToken          = w.AccessToken
                stored.refreshToken         = w.RefreshToken
                stored.accessTokenExpiresAt = now + w.ExpiresIn seconds
            return SIGNED_IN

        on ServerGoodbye g with g.ReasonType == UserAuthenticationFailed:
            if input == SESSION:
                forget stored.accessToken
                input = REFRESH if stored.refreshToken else CREDENTIAL
                continue
            if input == REFRESH:
                forget stored.accessToken and stored.refreshToken
                input = CREDENTIAL
                continue
            return FAILED(g.Reason)                               # stop; offer the user a retry button

        on ServerGoodbye g with g.AllowAutoReconnect:             # InternalServerError: no refusal, nothing decided
            if network_attempts == 5: return FAILED(g.Reason)
            network_attempts = network_attempts + 1
            wait min(2 ^ network_attempts, 30) seconds plus random jitter
            input = choose_input()                                # the same input and UniqueUserId; a refused refresh falls through later
            continue

        on ServerGoodbye g with any other ReasonType:
            return FAILED(g.Reason)                               # see the wire protocol guide

        on connection lost without ServerGoodbye:
            if network_attempts == 5: return FAILED("offline")
            network_attempts = network_attempts + 1
            wait min(2 ^ network_attempts, 30) seconds plus random jitter
            input = choose_input()                                # a lost refresh falls through to CREDENTIAL later
            continue

Why each rule exists:

  • A new connection, new MessageIds and a new ephemeral app key per attempt: the server closes the connection after a refusal, ignores a message whose MessageId it has already seen, and accepts each ephemeral app key once.
  • Replace the stored pair only when AccessToken is non-empty: SessionToken and legacy logins return empty token fields, and storing them would throw away a working refresh token.
  • Fall back to the credential after a refused refresh: this also covers a refresh whose ServerWelcome was lost (see Refresh tokens rotate).
  • Repeat the same input after InternalServerError, never after a refusal: the server decided nothing, and the same credential signs in the same user (with the same UniqueUserId, an adoption offer still stands), while repeating a refusal only repeats it.
  • Keep the stored tokens and the credential after InternalServerError: it is what the server answers during an outage, which exchanges no refresh token and refuses nothing. Drop a stored refresh token only after a UserAuthenticationFailed goodbye to the login that presented it.

The npm packages 2.0 behave this way on every connection, reconnects included (see Signing users in): one input per login, one fallback each after a refused access token and a refused refresh token, a new connection, new MessageIds and a new ephemeral app key from your applicationSecret function for every attempt, and the unique user id the server returns. A lost connection during the first connect() fails it instead of being retried; after a login, the client reconnects with its own backoff, up to 10 attempts. A goodbye that allows a reconnect, such as the InternalServerError of a server error during the login or of a sign-in the server could not decide yet, is retried with that backoff too, with the same input, also during the first connect(). Any other refusal stops the client: connect() rejects with a ConnectionClosedError that holds the goodbye, and onDisconnected reports it.

Requiring authenticated sessions

When Require authenticated sessions is on for an application, every login must present a SessionToken, a RefreshToken or an AuthProvider credential. A login that presents none of the three is refused with UserAuthenticationFailed and the Reason Your session could not be authenticated. Please sign in again. A presented token or credential is checked in exactly the same way whether the setting is on or off, and an invalid one is refused either way.

The setting also changes how users address each other. With it on, a user names another user by DisplayId, by one of their personal groups, or by answering a whisper, never by that user's unique user id: in ClientWhisperMessage.Recipient, the Subject of ClientSubscribeEventRequest and ClientUnsubscribeEventRequest, the OtherUserIds of ClientAddUsersToPersonalGroupRequest and ClientRemoveUsersFromPersonalGroupRequest, and the Recipient of the P2P requests, another user's unique user id is answered exactly like a name that belongs to nobody. A user's own unique user id still names that user, and calls from your server through the Server API are not affected. With the setting off, another user's unique user id is still accepted wherever a name is no user's DisplayId, because older clients rely on it; it is discouraged, so name users by DisplayId.

Turning the setting on refuses every client below protocol 2.0, whatever it sends, including every build that uses the Unity asset or the npm packages 1.8. Those clients cannot sign users in and cannot recover by reconnecting: their users are locked out until they install a protocol 2.0 client that signs in. A build with the npm packages 2.0 signs in only when it passes signIn or a stored session; without them it logs in the legacy way and is refused too.

Plan Default Can you switch it?
Free Off No
Subscription Off Yes, per application
Custom Off As agreed for your plan; by default no

Change it on the dashboard: open the application, go to Features and use the "Require authenticated sessions" switch. The switch is shown only on plans that let you change the setting.

Behaviour:

  • A change applies to new logins and to users addressing each other within seconds, in every region; at most 10 minutes if a region misses the change notice.
  • Users who are already connected stay connected: the login rule reaches them on their next login. The addressing rule applies to them from the change on.
  • There is no warn-only or report-only mode. On means refuse.
  • Switching the setting off accepts legacy logins again from the next login on.
  • If the server cannot read the application's setting during an incident, it treats the setting as off for that login or request.

Rollout checklist:

  1. Ship a protocol 2.0 client that signs in on every login (Device, Custom or both), follows the recommended client behaviour and names other users only by DisplayId. With the npm packages 2.0, that is a client with signIn that stores and resumes its session.
  2. Make that client send each user's old id as UniqueUserId on their first authenticated sign-in, so existing users are adopted (see Keeping existing users).
  3. Make your server use the right unique user id for every server call: the id it chose for the account, or the id the client got in ServerWelcome.UniqueUserId (see Knowing which user a client is).
  4. Test with the setting on in a second application, for example your staging application: first sign-in, reconnect within the hour, reconnect after the hour, logout, and a banned user.
  5. Move users off older builds: force an update of every client below protocol 2.0, including builds with the Unity asset or the npm packages 1.8, and of every client that does not sign in, and use your own analytics to confirm that almost all active users run the new build.
  6. Turn the setting on for the production application and watch your client error reports for UserAuthenticationFailed.
  7. To back out, turn it off again.

Your server and user identity

Your server never signs users in, never sees their tokens and never needs them. It works with unique user ids.

  • The unique user id is what your server uses. Your server sends the unique user id as uniqueUserId in REST calls and as ownerId for the user's leaderboard record. For a user on an older client it is the self-declared id, for an adopted user it is the old id, and for everyone else who signs in it is a new 64-character hex id, unless your server chose the id in advance (see below).
  • Five Minutes cannot tell your server who a client is. No REST endpoint verifies an access token, looks up the user behind a CustomId, lists or revokes sessions, or lists a user's sign-in methods, and there is no public JWKS. Do not send access or refresh tokens to your server. A unique user id that a client reports to your server is only a claim: a modified client can report any id, including someone else's.
  • Unique user ids stay between your server and the user's own client. Never send a user's unique user id to another user's client, and never write it into anything other users receive: a user's name or metadata, chat or whisper metadata, leaderboard metadata or a channel name. Users know each other by display id.
  • Bans keep a user out; revocation does not. To keep a user out, ban them through the Server API or the dashboard; a banned user's logins are refused (see When a login is refused). Signing a user out of their devices only ends sessions.

Knowing which user a client is

When your server acts for a user (writes their scores, reports their events, reads their configuration), it must know the unique user id of the account it authenticated. There are two ways; only the first is safe against a modified client.

Your server chooses the unique user id (recommended). Your server decides each account's unique user id before the account's first sign-in and makes Five Minutes adopt it. It then never has to trust an id reported by a client. This works with the Custom method:

  1. Your server derives the account's unique user id from its verified subject with a key only your server holds, for example "p_" + lowercase_hex( HMAC_SHA256(key, "playerid|" + subject) ) (66 characters; keep it to 128 characters or fewer so that it can own leaderboard records). Nobody else can compute it.
  2. Before the account's first sign-in, your server calls POST /api/v1/{applicationId}/user/register with that uniqueUserId (a key with Application.Users.Register is enough; see Pre-creating users). The call is harmless to repeat.
  3. Your server returns the CustomId, the Secret and the unique user id to the client over your own authenticated connection.
  4. The client signs in with AuthProvider = Custom, the pair, AuthCreate = true, and the unique user id in UniqueUserId. The first sign-in creates the user and adopts the id (see Keeping existing users); every later sign-in with the pair logs in as that user.
  5. ServerWelcome.UniqueUserId equals the id your server chose. If it does not, the adoption failed (for example because the pair had already signed in once without the id): the client should tell your server, and your server should treat the account as needing attention rather than accept the other id.

From then on your server uses the id it derived for every call about that account. Two conditions keep this safe: the id must stay secret, above all until the first sign-in has adopted it (derive it with your own key and hand it only to the account's own client), and Require authenticated sessions must be on (see Requiring authenticated sessions), because while it is off a legacy login that declares an adopted id acts as that user. Give guests a Custom pair too: let your server create an anonymous account for a new install and issue its pair, instead of using the Device method, so that every user your server deals with has an id it chose.

The client reports its unique user id (trust on first report). After each login the client reports ServerWelcome.UniqueUserId to your server over your own authenticated connection. Your server records the first id reported for each account, refuses a different id for the same account later, and refuses an id that is already recorded for another account. This is simple and works with the Device method, but a modified client can report a unique user id that no account has recorded yet, and your server cannot check it.

Pre-creating users

POST /api/v1/{applicationId}/user/register (scope Application.Users.Register, or Application.Admin.FullAccess, "Full access") creates a user with the uniqueUserId you choose, or returns the existing user with that id. A user created this way is a legacy user: the first credential sign-in that sends the same id in UniqueUserId and creates a new user adopts it (see Keeping existing users). This lets your server decide a user's unique user id before the user first signs in. Use ids nobody can guess, because adoption goes to the first sign-in that offers the id. A key with only Application.Users.Register can register Standard users and nothing else, so your account server does not need a Full access key; registering any other userType needs Full access and otherwise gets 403.

POST /api/v1/<application id>/user/register HTTP/1.1
Host: signalr.fiveminutes.cloud
Authorization: Bearer <server token>
Content-Type: application/json

{"uniqueUserId":"user-42","username":"Alice"}

The full request and response reference is in the Five Minute Server API.

Sign-in methods and devices on the dashboard

A user's page on the dashboard has a "Sign-in & devices" section:

  • Sign-in methods: the provider (Device or Custom id), the CustomId as you issued it or a keyed hash for a device (the raw device id is never stored), when it was linked and when it was last used.
  • Devices: one row per session, with the time of its last sign-in or refresh, its expiry or sign-out time, and its status (Active, Signed out, Expired).
  • Sign out ends one session and Sign out of all devices ends every active session of the user. Both work like a logout (see Revocation) and are available to the Owner, organization admin and application admin roles; every role that can open the user's page can view the section.
  • Signing a user out is not a ban. The user can sign in again at once.
  • A user who has only ever logged in the legacy way has no sign-in methods and no devices.

Ephemeral app keys and user identity

An ephemeral app key lets the client pass application admission without shipping the static application secret: your server mints a short-lived ecs_... key (scope Application.ClientCredentials.Mint, "Mint ephemeral app keys") and hands it to the client for one connection. It admits the application only and carries no user identity, and each key is accepted once (single use across all regions, best effort), so every reconnect needs a new key; details in the Five Minute Server API.

Calling the Server API

Your server calls https://signalr.fiveminutes.cloud/api/v1/... with Authorization: Bearer <server token>. Get the token with POST /api/v1/api-clients/authenticate and the body {"applicationId":"<application id>","clientSecret":"<API key secret>"}; it is valid for expiresInSeconds (3600) seconds and there is no refresh token. A missing, invalid or expired token gets 401 with an empty body; a token without the needed scope gets 403 with an empty body. Error bodies, rate limits and idempotency rules: Five Minute Server API.

Scopes used in this document:

  • Application.Users.Register: POST /api/v1/{applicationId}/user/register for Standard users.
  • Application.Admin.FullAccess ("Full access"): every route, including the ban endpoints.
  • Application.ClientCredentials.Mint ("Mint ephemeral app keys"): minting ephemeral app keys.

Reference: Five Minute Server API.

Message reference

Client prerequisites

These messages need a completed login: send them only after ServerWelcome. Anything sent earlier makes the server close the connection. Give every message a new MessageId. Each request gets a ServerAck as soon as the server has received it, when IsAckRequested is true, then one typed response whose ResponseToMessageId is your MessageId. The ServerAck says only that the request arrived; the typed response says whether it succeeded. Failures arrive in the typed response as Success=false with a FailureReason. The server never sends these responses to a connection below protocol 2.0.

This applies to ClientLogoutRequest, ClientLinkIdentityRequest and ClientUnlinkIdentityRequest. ClientUserInfoResponse is the login step itself: send it once per connection, in reply to ServerUserInfoRequest (see the login handshake). Conventions: Message conventions. The tables below give each message's fields and what they mean; the client library sends and reads the messages.

Present an identity at login (ClientUserInfoResponse)

Client -> server. Sent once per connection in reply to ServerUserInfoRequest. Reply: ServerWelcome (then ServerAck when IsAckRequested is true), or a ServerGoodbye (see When a login is refused).

Field Type Rules Meaning
UniqueUserId text Required, 1-256 characters. The stored unique user id, the old legacy id on a first sign-in, or any new value. Trusted only on a legacy login.
Username text Empty, or 3-64 letters, digits or -. Renames the user on this login when non-empty.
AssetVersion text Optional. Your client's version, for troubleshooting.
RuntimePlatform text Optional. Platform, for troubleshooting.
RuntimeVersion text Optional. Runtime version, for troubleshooting.
RuntimeMode text Optional. Runtime mode, for troubleshooting.
SystemLanguage text Optional. System language, for troubleshooting.
SessionToken text Empty, or an unexpired access token of this application. Branch 1: log in on an existing session.
RefreshToken text Used only when SessionToken is empty. Branch 2: rotate the session's tokens and log in.
AuthProvider text Empty, Device or Custom, exact spelling. Used only when both token fields are empty. Branch 3: sign in with a credential.
AuthDeviceId text Required for Device, at most 256 characters. The device credential.
AuthCustomId text Required for Custom, at most 256 characters. The Custom id your backend issued.
AuthSecret text Required for Custom, at most 1024 characters. The Custom secret your backend issued.
AuthCreate true/false The contracts library defaults to true. Create the user when the credential is unknown.
AuthUsername text Device only; follow the Username rules. Names or renames the user on a Device sign-in.
MessageId GUID New for every message, also for every login attempt. Duplicate detection.
IsAckRequested true/false Ask for a ServerAck.
using FiveMinutes.Model.Messages.Common;
using FiveMinutes.Model.Messages.Client;

// First sign-in on this install: exactly one input, a Device credential.
var signIn = new ClientUserInfoResponse
{
    UniqueUserId = storedPlayerId ?? legacyId ?? Guid.NewGuid().ToString(),
    Username = null,                         // keep the current name
    AuthProvider = AuthProviderNames.Device, // "Device"
    AuthDeviceId = deviceId,
    AuthCreate = true,
    AuthUsername = "Alice"
};

// Sign-in through your backend with the pair it issued.
var customSignIn = new ClientUserInfoResponse
{
    UniqueUserId = storedPlayerId ?? legacyId ?? Guid.NewGuid().ToString(),
    AuthProvider = AuthProviderNames.Custom, // "Custom"
    AuthCustomId = customId,
    AuthSecret = secret,
    AuthCreate = true
};

// Reconnect while the access token is valid.
var resume = new ClientUserInfoResponse { UniqueUserId = storedPlayerId, SessionToken = storedAccessToken };

// Reconnect after the access token expired.
var refresh = new ClientUserInfoResponse { UniqueUserId = storedPlayerId, RefreshToken = storedRefreshToken };

The contracts library checks the message before it sends it and throws ArgumentException instead of sending:

Exception message Cause Client action
UniqueUserId must be set. UniqueUserId is empty. Always send the stored id, the legacy id or a new GUID.
Unique user id must not exceed 256 characters UniqueUserId is too long. Shorten it.
Invalid username '<name>'. ... Username breaks the name rules. Send a valid name or none.
AuthDeviceId must be set when AuthProvider is 'Device'. Missing device id. Set AuthDeviceId.
AuthCustomId and AuthSecret must be set when AuthProvider is 'Custom'. Missing Custom field. Set both.
Unknown AuthProvider '<value>'. Supported values: 'Device', 'Custom'. Wrong provider name. Use AuthProviderNames.Device or AuthProviderNames.Custom.

The npm packages 2.0 build this message themselves on every login, from their uniqueUserId, username, signIn and session options and the stored tokens, and check the same rules before connecting, and the message itself before sending it: connect() rejects with a ClientConfigError whose field names the option, such as signIn.customId (see Checks before connecting).

Receive the resolved identity (ServerWelcome)

Server -> client. Answers ClientUserInfoResponse when the login succeeds. It is not a typed response: it has no ResponseToMessageId, Success or FailureReason.

Field Type Rules Meaning
AvailableChannels list of ChannelInfo Deprecated. See Channel information. Channels the user can see.
DisplayId text Always set. The user's display id.
AccessToken text Non-empty only on RefreshToken and AuthProvider logins. The new access token.
RefreshToken text Non-empty only on RefreshToken and AuthProvider logins. The new refresh token; the previous one of this session no longer works.
ExpiresIn whole number 3600 when tokens were issued, otherwise 0. Lifetime of AccessToken in seconds from receipt.
UniqueUserId text Always set, on every branch. The unique user id. Always adopt it.
using FiveMinutes.Model.Messages.Server;

void OnWelcome( ServerWelcome welcome )
{
    store.PlayerId = welcome.UniqueUserId;               // always
    if( !string.IsNullOrEmpty( welcome.AccessToken ) )   // new pair only on refresh and credential logins
    {
        store.AccessToken = welcome.AccessToken;
        store.RefreshToken = welcome.RefreshToken;
        store.AccessTokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds( welcome.ExpiresIn );
    }
    store.DisplayId = welcome.DisplayId;
}

End the session (ClientLogoutRequest)

Client -> server, only after ServerWelcome. Reply: ServerAck, then ServerLogoutResponse. Behaviour: Signing out.

Field Type Rules Meaning
RefreshToken text Required; the contracts library refuses an empty one. The refresh token of the session to end.
MessageId GUID New for every message. Correlates the response.
IsAckRequested true/false Ask for a ServerAck.

ServerLogoutResponse fields

Field Type Rules Meaning
ResponseToMessageId GUID The MessageId of your request.
Success true/false true when the session has ended, now or earlier.
FailureReason text Empty on success. See the table in Signing out.
var logout = new ClientLogoutRequest { RefreshToken = store.RefreshToken };
// send it, then on ServerLogoutResponse r with r.ResponseToMessageId == logout.MessageId:
//   delete the stored tokens whatever r.Success says, then disconnect

The contracts library throws ArgumentException with RefreshToken must be set. instead of sending when RefreshToken is empty or only spaces.

Add a sign-in method (ClientLinkIdentityRequest)

Client -> server, only after ServerWelcome, on a connection that holds a session. Reply: ServerAck, then ServerLinkIdentityResponse. Behaviour and error codes: Link a sign-in method.

Field Type Rules Meaning
Provider text Device or Custom, exact spelling. The provider of the credential to add.
DeviceId text Required for Device, at most 256 characters. The device credential.
CustomId text Required for Custom, at most 256 characters. The Custom id your backend issued.
Secret text Required for Custom, at most 1024 characters. The Custom secret your backend issued.
MessageId GUID New for every message. Correlates the response.
IsAckRequested true/false Ask for a ServerAck.

ServerLinkIdentityResponse fields

Field Type Rules Meaning
ErrorCode text Empty on success. Machine-readable failure code.
ResponseToMessageId GUID The MessageId of your request.
Success true/false true when the credential is linked to this user.
FailureReason text Empty on success. Description of the failure.
var link = new ClientLinkIdentityRequest
{
    Provider = AuthProviderNames.Custom,
    CustomId = customId,
    Secret = secret
};
// on ServerLinkIdentityResponse r with r.ResponseToMessageId == link.MessageId:
//   r.Success, or switch on r.ErrorCode ("NotAuthenticated", "IdentityAlreadyLinked", ...)

The contracts library throws ArgumentException, and the npm packages 2.0 reject with a MessageValidationError, instead of sending when a field is missing or the provider name is wrong: DeviceId must be set when Provider is 'Device'., CustomId and Secret must be set when Provider is 'Custom'. or Unknown Provider '<value>'. Supported values: 'Device', 'Custom'.

Remove a sign-in method (ClientUnlinkIdentityRequest)

Client -> server, only after ServerWelcome, on a connection that holds a session. Reply: ServerAck, then ServerUnlinkIdentityResponse. Behaviour and error codes: Unlink a sign-in method.

Field Type Rules Meaning
Provider text Device or Custom, exact spelling. Every sign-in method of this provider is removed.
MessageId GUID New for every message. Correlates the response.
IsAckRequested true/false Ask for a ServerAck.

ServerUnlinkIdentityResponse fields

Field Type Rules Meaning
ErrorCode text Empty on success. Machine-readable failure code.
ResponseToMessageId GUID The MessageId of your request.
Success true/false true when the provider's sign-in methods were removed.
FailureReason text Empty on success. Description of the failure.
var unlink = new ClientUnlinkIdentityRequest { Provider = AuthProviderNames.Device };
// on ServerUnlinkIdentityResponse r: "LastIdentity" means link another method first

The contracts library throws ArgumentException, and the npm packages 2.0 reject with a MessageValidationError, with Unknown Provider '<value>'. Supported values: 'Device', 'Custom'. instead of sending any other provider name.

End-to-end walkthrough

An app adds sign-in: guests sign in with Device, and users who create an account get a Custom sign-in method that works on every device.

  1. Your server (once): creates a random 32-byte key for deriving Custom pairs and keeps it only on the server.
  2. Client (first launch): generates a random GUID as the device id and persists it. It connects, completes the application step, and on ServerUserInfoRequest sends ClientUserInfoResponse with UniqueUserId = the old legacy id if the app had one (otherwise a new GUID), AuthProvider = Device, AuthDeviceId, AuthCreate = true. Success: ServerWelcome with a non-empty AccessToken. It stores the pair, the expiry and UniqueUserId.
  3. Client: reports ServerWelcome.UniqueUserId to your server over your own connection. Your server: records it for the user's account (trust on first report, see Knowing which user a client is) and uses it as uniqueUserId and ownerId in its calls from now on.
  4. Client (restart 20 minutes later): sends only SessionToken. Success: ServerWelcome with empty token fields and the same UniqueUserId; the stored pair stays.
  5. Client (next day): sends only RefreshToken. Success: ServerWelcome with a new pair; it replaces the stored pair at once.
  6. The user creates an account. Client: signs in to your system and calls your server. Your server: verifies the user and returns the derived CustomId and Secret. Client: sends ClientLinkIdentityRequest with Provider = Custom. Success: ServerLinkIdentityResponse with Success = true. On NotAuthenticated, it reconnects with the refresh token first and sends the link again.
  7. Second device: the client signs in to your system, gets the same pair from your server, and sends ClientUserInfoResponse with AuthProvider = Custom, AuthCreate = false. Success: ServerWelcome.UniqueUserId equals the id from step 2.
  8. The user signs out on the second device. Client: sends ClientLogoutRequest with its refresh token. Success: ServerLogoutResponse with Success = true; it deletes its tokens and disconnects. The first device stays signed in.
  9. Your organization: once nearly all active users run this client, turns on Require authenticated sessions for the application (see the rollout checklist).

Walkthrough: your backend signs every user in

An app whose own backend has accounts: the backend decides who may connect and which unique user id each account has, and it acts for users over the Server API. It uses an ephemeral app key per connection, the Custom method, and unique user ids the backend chooses (see Knowing which user a client is).

  1. Your server (once): creates a random 32-byte key for deriving Custom pairs and unique user ids, and keeps it only on the server. In the dashboard, your team creates an API key with "Mint ephemeral app keys" and the other scopes your server needs, and turns on Require authenticated sessions once no older client is in use.
  2. Client: signs the user in to your system and asks your backend for a connection ticket over your own authenticated connection.
  3. Your server: verifies the user and derives the CustomId, the Secret and the unique user id from the account's subject. The first time it sees the account, it calls POST /api/v1/{applicationId}/user/register with that unique user id (Full access). It mints an ephemeral app key with POST /api/v1/{applicationId}/client-credentials/ephemeral and {"ttlSeconds":120}. It answers the ticket request with the application ID, the host, the ephemeral app key, the CustomId, the Secret and the unique user id. Rate-limit this endpoint per account and per IP address (see Protecting your mint endpoint).
  4. Client: connects to the host and, on ServerCredentialsRequest, sends ClientCredentialsResponse with the application ID and the ephemeral app key as ApplicationSecret. Success: ServerUserInfoRequest.
  5. Client: sends ClientUserInfoResponse with UniqueUserId = the unique user id from the ticket, AuthProvider = Custom, AuthCustomId, AuthSecret, AuthCreate = true, both token fields empty. Success: ServerWelcome with UniqueUserId equal to the unique user id and a non-empty AccessToken. The client stores the token pair and its expiry; it can forget the Secret.
  6. Client (every later connection): asks your backend for a new ticket, which now only needs a new ephemeral app key, and logs in with SessionToken or RefreshToken as in Reconnecting. When both tokens are refused, it asks for a ticket with the pair again and signs in with Custom (AuthCreate = false).
  7. Your server: acts for the user with the unique user id it derived: POST /api/v1/events with uniqueUserId, POST /api/v1/leaderboards/{leaderboardId}/records with ownerId, GET /api/v1/remote-config?uniqueUserId=. It never needs an id reported by the client.

With the npm packages 2.0, steps 4 to 6 are the client's configuration. The applicationSecret function asks your backend for a new ephemeral app key before every connection; the signIn function gives the pair only when the client holds no usable token; uniqueUserId offers the id your server chose for adoption:

import { ChatClient } from "@fiveminutes-io/chat-client";

// From your backend's ticket (steps 2 and 3): the host, the pair and the unique user id it chose
const client = new ChatClient({
  serverUrl: account.host,                          // for example https://signalr.fiveminutes.cloud
  applicationId: "<application id>",
  applicationSecret: () => fetchEphemeralKey(),     // a new key for every connection, reconnects included
  signIn: () => ({ provider: "Custom", customId: account.customId, secret: account.secret }),
  uniqueUserId: account.uniqueUserId,
  session: storedSession,                           // the stored token pair, if any
});
client.onSessionChanged((session) => storeSession(session));
await client.connect();

Check after the first sign-in that client.getSession()?.uniqueUserId equals the id from the ticket (step 5).

Limits

Limit Value What happens when exceeded
UniqueUserId 1-256 characters The contracts library refuses to send the message; the npm packages 2.0 refuse it before connecting (ClientConfigError); the server ends such a login with a RequestFormatViolation goodbye.
Username 3-64 characters, each a letter, a digit or - The contracts library refuses to send the message; the npm packages 2.0 refuse it before connecting (ClientConfigError); the server ends such a login with a RequestFormatViolation goodbye.
AuthUsername Follow the Username rules Not checked by the contracts library; the npm packages 2.0 refuse an invalid signIn.username before connecting. The server refuses a Device sign-in whose AuthUsername is longer than 64 characters with UserAuthenticationFailed (Player authentication failed: the user name is longer than 64 characters.).
AuthDeviceId / DeviceId At most 256 characters Not supported. The npm packages 2.0 refuse a longer signIn.deviceId before connecting.
AuthCustomId / CustomId At most 256 characters Not supported. The npm packages 2.0 refuse a longer signIn.customId before connecting.
AuthSecret / Secret At most 1024 characters Not supported. The npm packages 2.0 refuse a longer signIn.secret before connecting.
Access token lifetime 3600 seconds (the server allows up to 1 minute past expiry for clock differences) A login presenting it is refused with UserAuthenticationFailed.
Refresh token lifetime 30 days after issue or last use A login presenting it is refused; sign in with the credential.
Refresh token uses 1 A second use is refused.
Access token revocation spread Usually seconds, allow up to 2 minutes; if it cannot be distributed, until the token expires (at most 3600 seconds after issue, plus the 1-minute clock allowance) The token is still accepted on new logins until then.
Require authenticated sessions change Seconds; at most 10 minutes New logins, and users addressing each other, follow the old value until then.
Automatic retries of a sign-in None after a refusal; after a goodbye that allows a reconnect, bounded (for example 5 attempts, backoff capped at 30 seconds) Set by your client. Retrying a refusal only repeats it.

Troubleshooting

Symptom Cause Fix
All users are refused right after turning on Require authenticated sessions. Clients below protocol 2.0, including builds with the Unity asset or the npm packages 1.8, or clients that log in without a token or credential, such as a build with the npm packages 2.0 without signIn. Turn the setting off. Ship a protocol 2.0 client that signs in, then follow the rollout checklist.
Whispers, event subscriptions, personal-group changes or P2P requests that name a user by unique user id stop reaching them after turning on Require authenticated sessions. With the setting on, another user's unique user id names nobody (see Requiring authenticated sessions). Name users by DisplayId.
Reconnects fail with UserAuthenticationFailed about an hour after sign-in. The client sends an expired access token as SessionToken, possibly together with the refresh token. Send SessionToken only before its expiry minus a margin; after that send only RefreshToken.
A refresh is refused although the token is less than 30 days old. It was already used (by another process, or the ServerWelcome after a refresh was lost), or the session was revoked. Sign in with the credential. Keep one token pair per install.
A sign-in ends with InternalServerError and AllowAutoReconnect true. The server could not sign the user in, or could not decide the sign-in yet, for example because a service it needs did not answer. Nothing was decided about the credential: an outage is no refusal. Keep the stored tokens and the credential. Log in again with the same input and the same UniqueUserId after a bounded backoff; the same credential signs in the same user, the same refresh token refreshes, and an adoption offer still stands. Contact support if it persists.
Link or unlink answers NotAuthenticated. The connection logged in the legacy way, logged out, its access token expired (after a SessionToken login: the presented token's expiry), or the session was revoked. Reconnect with the refresh token (or the credential), then send the request again.
One person now has two user accounts after an unlink. The device's Device method was unlinked, and its next Device sign-in with AuthCreate true created a new user. That device now belongs to the new user and cannot be linked back, because users are never merged. On that device, sign in with the remaining method (for example Custom) from now on. Before unlinking Device, switch the device to that method.
A new user appeared instead of the old one on the first sign-in. An adoption condition failed: the old id was not sent, the credential already belonged to a user, AuthCreate was false, no user had that id, or another sign-in adopted it first. Users are never merged. Fix the client to send the old id in UniqueUserId before most users update.
A Custom sign-in that used to work is refused with "Player authentication failed: credential rejected." The secret changed (a third-party token was used as the secret, or your derivation key changed), or the user is banned. Derive the secret from a stable subject with a key that never changes. To store a new secret for a user who also has another sign-in method: log in (with a token or that other method), unlink Custom (this removes every Custom id of the user), then link the same CustomId with the new secret.
The server ignores the UniqueUserId the client sends. On token and credential logins the server decides the user; the id you send is at most an adoption hint. Always use ServerWelcome.UniqueUserId.
The contracts library throws ArgumentException when sending. Empty or too long UniqueUserId, invalid Username, wrong provider name, missing credential fields, or an empty logout RefreshToken. Fix the field named in the exception message.
With the npm packages 2.0, connect() rejects at once with ClientConfigError. A sign-in or login value breaks a rule, for example an e-mail address as username, an empty signIn.deviceId or a provider name in lower case. Fix the option named in error.field.
A banned user is refused with InvalidCredentials instead of UserAuthenticationFailed. Bans on SessionToken and legacy logins surface as InvalidCredentials. Treat InvalidCredentials after ClientUserInfoResponse as final.

Related documents

  • Five Minute wire protocol: client libraries, the login handshake, message conventions and every reason the server closes a connection.
  • Five Minute API Contracts: the field-level reference of every protocol 2.0 message and type.
  • Five Minute Server API: server tokens, scopes, ephemeral app keys, the register and ban endpoints, errors and rate limits.
  • Five Minute Leaderboards: boards whose records are owned by the unique user id.
  • Five Minute Remote Config: remote config, experiments and metric events, assigned and reported per unique user id.
  • npm packages 2.0: the JavaScript client, which signs users in, keeps their session and logs them out.
  • Unity Asset: the Unity asset, which speaks protocol 1.7 or earlier (asset 1.20 speaks 1.6, asset 1.23 speaks 1.7) and does not sign users in.