Cross-Platform Identity for Roblox and Web Companions: Session Tokens That Survive Platform Hops

You probably think of cross-platform login as a single sign-on problem solved by OAuth and a cookie. However, the moment a Roblox experience hands a player off to a web companion — or a web companion deep-links back into a place — the cookie model collapses and you are left holding a session-bridging problem that looks a lot more like service-to-service auth than consumer login.
Our prior post on cross-platform player identity covered the profile layer: how to reconcile a Roblox UserId with a web account, what to store, and how to keep the two halves of a player's identity in sync. This post drills one level lower into the auth/session plumbing that makes that profile layer usable in production.
What is session token bridging? Session token bridging is the mechanism that lets a logged-in player move between a Roblox experience and a web companion without re-authenticating. It typically uses a short-lived signed token minted by a trusted backend, exchanged for a platform-native session on the destination, with a refresh token held server-side for rotation and revocation.
Why The Cookie Model Breaks At The Platform Boundary
A web companion authenticates the way every other web app does: an HTTP-only cookie scoped to the companion's domain, refreshed against an identity provider, and invalidated on logout. Roblox does none of this — there is no shared cookie jar, no shared origin, and the HttpService client running inside a place is not a browser.
What you have instead is a Roblox client that knows a UserId (from Players.LocalPlayer), a Roblox server that can call out via HttpService, and a web companion that knows an account ID from your own auth system. Bridging those three identities into one continuous session is the actual problem.
Why can't you just share a cookie between Roblox and your web app? Roblox clients are not browsers and do not participate in your companion's cookie jar. All outbound HTTP from a Roblox experience runs server-side through HttpService, which has no concept of a user agent's session. Any bridge has to flow through your backend — there is no first-party cookie path.
The Three-Token Pattern
The pattern that holds up in production uses three distinct tokens with three different lifetimes. Keep them separate — collapsing two of them into one is where most of the security incidents start.
The first is a platform attestation — short-lived proof that the request really came from a Roblox server acting on behalf of a specific UserId. The second is a bridge token — a single-use, very short-lived JWT minted by your backend, scoped to one destination platform. The third is the session refresh token — the long-lived, revocable credential that lets either platform mint new access tokens without re-prompting the player.
| Token | Lifetime | Issued by | Used to |
|---|---|---|---|
| Platform attestation | 30–60 seconds | Roblox server (HMAC over UserId + nonce) | Prove origin to your backend |
| Bridge token (JWT) | 60–120 seconds | Your auth service | Hand off to the destination platform once |
| Refresh token | 7–30 days, rotated | Your auth service | Mint new access tokens on demand |
Each token answers a different question. The attestation answers who is asking, the bridge token answers what they are allowed to do in the next 90 seconds, and the refresh token answers are they still a valid player in our system at all.
Minting The Bridge Token
The bridge token is the one that crosses the platform boundary, so it has to be small, signed, and short-lived. Use a JWT with a tight claim set — sub (your internal account ID), roblox_uid, aud (the destination platform), jti (unique ID for replay defense), and exp set 60 to 120 seconds in the future.
Sign it with an asymmetric key (EdDSA or ES256) so the destination can verify without holding the private key. Symmetric HMAC works at small scale, but the moment you have more than one verifier — a web companion, a mobile app, a partner experience — you want signature verification to require only a public key.
How long should a bridge token live? Sixty to one-hundred-twenty seconds is the right range. The token has to survive a player tapping a deep link, the destination platform loading, and a single round-trip to your auth service — but no longer. Anything past two minutes turns a leaked bridge token into a usable credential.
The Hop From Roblox To Web Companion
The flow looks straightforward on paper. The Roblox server calls your auth service with the platform attestation, your service responds with a one-time bridge token, the Roblox client opens the companion URL with the token in the URL fragment, the companion exchanges the token for a session cookie, and the player is logged in.
The details are where it gets interesting. The URL fragment matters — putting the bridge token in the query string sends it to the companion's web server access logs, which is exactly the kind of credential leak a refresh-token rotation strategy is supposed to prevent. Use the fragment (#bridge=...) and have client-side JavaScript POST it to the exchange endpoint.
The companion then does three things: verifies the JWT signature, checks the jti against a short-lived replay cache (Redis with a 5-minute TTL is plenty), and exchanges the bridge token for a normal session cookie. After that, the player is in the same auth state they would be in if they had logged in directly.
The Hop From Web Companion To Roblox
Going the other direction is harder because Roblox does not give you a clean place to inject a session. The web companion mints a bridge token the same way, but instead of redirecting to a URL, it has to hand the token to the Roblox client through a mechanism the Roblox runtime can read.
The two patterns that work are a launch-data payload (encoded into the join URL via JoinScript or a place-launching deep link with query params the experience reads on join) and a backend rendezvous. The rendezvous pattern is more flexible: the companion stores the bridge token against a short-lived rendezvous code, the player joins the place with the code, and the Roblox server exchanges the code for the token over HttpService.
What is the rendezvous pattern? The rendezvous pattern stores a bridge token against a short code on your backend, hands the player only the code, and lets the destination server exchange the code for the real token. It keeps the credential out of URLs, launch data, and client logs — useful when the destination platform has no secure transport for a JWT.
Refresh Token Rotation And Revocation
The refresh token is the long-lived secret, and it has to be treated like one. Rotate it on every use — issue a new refresh token alongside every access token, and invalidate the old one. This makes a stolen refresh token detectable: if the legitimate client and the attacker both try to use it, one of them gets a token-reuse error and you can force a full re-auth.
Store the refresh token server-side, indexed by its jti, with the account ID, issued-at, last-rotated-at, and a revocation flag. Revocation is the feature that most teams skip until they need it — and the day you need it (compromised account, leaked database, player charge-back) is exactly the day you cannot afford to be writing the revocation path from scratch.
What Goes Wrong In Production
The failure modes are predictable. Clock skew between your auth service and a Roblox server kills bridge-token validation if you set exp too tightly — allow at least 30 seconds of leeway on the verifier side. Replay attacks against the bridge endpoint kill you if the jti cache is per-instance instead of shared — use Redis or DynamoDB, not in-memory.
The subtler failure is silent identity drift. A player changes their email on the web companion, your account record updates, but the cached roblox_uid mapping in the Roblox DataStore still points at the old account. This is the same class of stale-state read that haunts cross-platform save state — the answer is the same: treat the Roblox-side cache as derived data, refresh it on every session bridge, and never trust it as the source of truth.
Tying It Into The Roblox Server
On the Roblox side, the bridge lives in a server script that talks to your auth service through HttpService. Keep the bridging logic out of the place's normal server architecture — it is auth, not gameplay, and it should be the only module that knows your auth service exists.
Expose a single internal API: Auth.getBridgeTokenFor(player, destination). The rest of your codebase asks for a token when it needs one (opening a companion link, joining a partner experience) and never touches the JWT, the refresh logic, or the platform attestation directly.
Should the Roblox client ever see the refresh token? No. The refresh token lives on your backend, indexed against the player's account ID. The Roblox server requests bridge tokens on the player's behalf and never exposes the refresh token to the client. Treating the Roblox client as untrusted is the same posture you take for anti-exploit work — assume any value the client can read can leak.
Observability And Audit
Every bridge issuance, every exchange, every refresh rotation, and every revocation needs to be logged with the account ID, the source platform, the destination platform, the jti, and the result. This is not optional — it is what lets you answer the question "how did this account end up logged in from two places at once?" when a player support ticket lands on your desk.
Track P95 latency on the bridge-mint and bridge-exchange endpoints separately. Bridge-mint latency hides in the join-flow time the player perceives as "the experience taking forever to load" — keep it under 200ms or the hop feels broken even when it works.
FAQ
Can I use OAuth directly between Roblox and my web companion?
Not cleanly. Roblox does not expose an OAuth client surface inside a place, and the standard OAuth redirect flow assumes a browser. You can use OAuth between your backend and an external identity provider, but the Roblox-to-companion hop still needs a bridge token your backend mints.
What signing algorithm should I use for bridge tokens?
EdDSA (Ed25519) for new systems, ES256 if your verifier libraries do not yet support EdDSA. Avoid RS256 unless you have a specific reason — the signatures are large and slow to verify, which matters when the verifier is a Roblox server paying per-byte for HttpService round-trips.
How do I handle a player who is logged into the companion but not into Roblox?
Treat the companion session as the source of truth. When the player joins the Roblox experience, the server calls your backend with the platform attestation, your backend matches the Roblox UserId against the linked account, and if a companion session exists you mint a bridge token automatically.
What happens when a player unlinks their Roblox account from the companion?
Revoke the refresh token and invalidate the Roblox-side identity cache immediately. The next bridge request fails closed, the player is treated as unauthenticated on both sides, and any in-flight bridge tokens expire within 60–120 seconds — which is why you keep the bridge token lifetime short.
Do I need a separate refresh token per platform?
Yes. Keep one refresh token per (account, platform) pair so you can revoke a single device without forcing a global re-auth. The account-level revocation flag is the master switch; per-platform tokens are the scalpel.
If You Are Wiring This Up Now
If you are scoping the session layer for a Roblox experience plus a web companion and want a second set of eyes on the bridge design, the team at iSimplifyMe builds and operates cross-platform identity systems across consumer and enterprise environments. Reach out for a working session — we'll map your token flow, name the failure modes you're about to hit, and leave you with a deployable plan that covers issuance, rotation, revocation, and audit.


