# 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](/docs/authentication.md).

The model calls still run on **your backend**, with a server SDK
([Node.js](/docs.md), [Python](/docs/python.md), or [.NET](/docs/dotnet.md)). 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](/docs/authentication.md),
and turn on the header source of the session middleware.

Node.js:

```ts
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):

```python
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:

```csharp
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**:

```text
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.

```csharp
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:

```csharp
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](/docs/authentication.md) instead.

## Other engines and native apps

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

| Route                                                                                      | What it does                                                                                                           |
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `GET /start?mode=loopback&port=&state=&code_challenge=&code_challenge_method=S256`         | Open 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=S256` | The 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` header                                            | Ends 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`.
