Enlitt

Client sign-in for games and native apps

Sign players in from Unity, Godot, desktop, and mobile apps through your own backend, with no Enlitt key in the client.

Use this page when your users run something that is not a web page: a Unity or Godot game, a desktop app, or a mobile app. Web pages use the normal cookie login.

The model calls still run on your backend, with a server SDK (Node.js, Python, or .NET). The client never holds an Enlitt key. It only signs the player in and then sends a session header to your backend.

How it works

  1. The game opens the system browser at your backend's /start route.
  2. The player signs in to Enlitt. Enlitt sends them back to your normal https callback, which you already registered.
  3. Your backend hands a one-time code back to the game, through one of three modes:
    • loopback (desktop and the editor): a redirect to a port on 127.0.0.1 that the game listens on;
    • deep link (mobile): a redirect to your app's link;
    • pairing (consoles, TVs, or any device without a browser): the player types a short code on their phone instead.
  4. The game redeems the code at your backend's /token route and receives the session token.
  5. The game sends Enlitt-Session: <token> on every request to your backend. Your backend reads it with the session middleware and makes model calls as usual.

PKCE protects every mode, so a code that is intercepted is useless without the game's verifier.

Backend: mount the routes

Mount the client sign-in handler next to your login handlers, and turn on the header source of the session middleware.

Node.js:

import { createClientSignInHandler, createMemoryClientSignInStore } from "@enlitt/sdk/server";
import { enlittSessionMiddleware } from "@enlitt/sdk/core";

const clientSignIn = createClientSignInHandler({
  callback: config, // the same CallbackConfig your login handlers use
  basePath: "/enlitt/client",
  appName: "My Game",
  returnUrls: ["com.example.mygame:/signed-in"], // only if you use deep links
  pairing: true, // only if you support devices without a browser
  store: createMemoryClientSignInStore() // development only; use a shared store in production
});
// Route every path under /enlitt/client to clientSignIn.

const withSession = enlittSessionMiddleware({ header: true });

Python (FastAPI):

from enlitt.asgi import EnlittSessionMiddleware
from enlitt.client_sign_in import ClientSignInConfig, MemoryClientSignInStore
from enlitt.starlette import enlitt_routes

client_sign_in = ClientSignInConfig(
    callback=config,
    base_path="/enlitt/client",
    app_name="My Game",
    pairing=True,
    store=MemoryClientSignInStore(),
)
app = FastAPI(routes=enlitt_routes(config=config, client_sign_in=client_sign_in))
app.add_middleware(EnlittSessionMiddleware, header=True)

ASP.NET Core:

app.UseEnlittSession(new EnlittSessionMiddlewareOptions { Header = true });
app.MapEnlittAuth(config);
app.MapEnlittClientSignIn(new ClientSignInConfig
{
    Callback = config,
    BasePath = "/enlitt/client",
    AppName = "My Game",
    Pairing = true,
    Store = new MemoryClientSignInStore(),
});

Notes for the backend:

  • Store. The in-memory store works only in a single process. With more than one server, give it a shared store (Redis, your database) that implements set, get, and consume.
  • Rate limits. Rate-limit POST /enlitt/client/pair and POST /enlitt/client/token per IP address with the limiter you already use.
  • Website sign-out. Signing in a game signs that browser out of your website, because the session moves to the game.
  • Deep links. returnUrls must list each deep link exactly. Use an https app link or a reverse-DNS custom scheme such as com.example.mygame:/signed-in.
  • Enlitt registration. No new redirect URL is registered with Enlitt. Your existing https callback is the only one.

Unity

Install the kit from the Package Manager with Add package from git URL:

https://github.com/enlitt/sdk.git?path=packages/unity-signin/upm

Pin a version by appending its tag, for example #unity-signin-v0.1.0. It needs Unity 2021.3 or later.

using Enlitt.SignIn;
using UnityEngine.Networking;

var signIn = new EnlittSignIn(new EnlittSignInOptions
{
    BackendBaseUrl = "https://api.mygame.example/enlitt/client",
    ReturnUrl = "com.example.mygame:/signed-in", // mobile only; omit on desktop
});

if (!signIn.IsSignedIn)
    await signIn.SignInAsync(); // loopback on desktop and in the editor, deep link on mobile

var request = UnityWebRequest.Get("https://api.mygame.example/api/npc-reply");
signIn.Authorize(request); // adds the Enlitt-Session header
await request.SendWebRequest();

For a device without a browser:

var pairing = await signIn.StartPairingAsync();
ShowOnScreen($"Go to {pairing.VerificationUri} and enter {pairing.UserCode}");
await pairing.WaitAsync();

Call SignOutAsync() to sign out. If your backend reports that the session is no longer valid (Enlitt answered personal_login_required), call ForgetSession() and sign in again.

  • Token storage. The kit stores the token in PlayerPrefs, which is not encrypted. For a shipped mobile game, pass TokenStorage an implementation backed by the platform keychain.
  • WebGL. A WebGL build is a web page, so it uses the normal cookie login instead.

Other engines and native apps

Any client can speak the same routes. All of them are under your basePath.

RouteWhat it does
GET /start?mode=loopback&port=&state=&code_challenge=&code_challenge_method=S256Open this in the system browser. It ends at http://127.0.0.1:<port>/callback?code=&state=, or error=access_denied.
GET /start?mode=redirect&redirect_uri=&state=&code_challenge=&code_challenge_method=S256The same, ending at your exact deep link.
POST /pairing with {"code_challenge", "code_challenge_method": "S256"}Returns {pairing_id, user_code, verification_uri, expires_in, interval}. Show the code and the URL to the player.
POST /token with {"grant_type": "code", "code", "code_verifier"}Returns {session_token}. A code works once, within 5 minutes.
POST /token with {"grant_type": "pairing", "pairing_id", "code_verifier"}Poll every interval seconds. It answers authorization_pending until the player confirms, then {session_token}.
POST /logout with the Enlitt-Session headerEnds the session. It always answers 204.
  • PKCE. code_verifier is 43 to 128 random URL-safe characters. code_challenge is the unpadded base64url SHA-256 of it.
  • State. state is a random value you choose and check when the code comes back.
  • Errors. Every /token error is 400 with {"error": ...}. The values are invalid_request, authorization_pending, access_denied, expired_token, and invalid_grant.

Send the token only to your own backend, as Enlitt-Session.

On this page