Unity quickstart

The Unity package signs players in without a browser inside your game. Players approve a short code on their phone or PC, or start as guests and add an email address later.

Before you start

  • A Game (device sign-in) application in the console. Tickoffline_access under Scopes so players stay signed in between five-minute access tokens. See Console onboarding.
  • Its Client ID, and the Project ID from project settings.
  • Unity 2022.3 or later. The package is verified on Unity 6.3 LTS (6000.3).

1. Add the package

The package dev.auth4.sdk lives in packages/sdk-unity. Add it from disk in the Package Manager, or reference it in Packages/manifest.json as the example project does:

examples/unity/Packages/manifest.json
{
  "dependencies": {
    "dev.auth4.sdk": "file:../../../packages/sdk-unity",
    "com.unity.modules.imgui": "1.0.0",
    "com.unity.modules.unitywebrequest": "1.0.0"
  }
}

Native applications are public clients: there is no client secret to embed, and the SDK can't send one.

2. Create the client

From the Device Login sample, which you can import from the Package Manager:

Excerpt from packages/sdk-unity/Samples~/DeviceLogin/DeviceLoginSample.cs
    _client = new Auth4Client(new Auth4ClientOptions
    {
        TenantId = tenantId,
        ClientId = clientId,
        AuthOrigin = authOrigin,
        Transport = new UnityWebRequestTransport(),
    });
    _client.SessionChanged += session =>
        _status = session == null ? "Signed out." : (session.IsGuest ? "Playing as guest." : "Signed in.");
}

AuthOrigin is the origin of your issuer, without /t/…. HTTP is only accepted for loopback addresses during local development. UnityWebRequestTransport works on every Unity platform. The default HttpClientTransport is meant for headless tools and tests.

3. Sign in with another device

SignInWithDeviceAsync starts device authorization, passes you the code to show, and polls until the player approves, denies or the code expires:

Excerpt from packages/sdk-unity/Samples~/DeviceLogin/DeviceLoginSample.cs
private async void SignInWithDevice()
{
    _cancellation = new CancellationTokenSource();
    try
    {
        _status = "Waiting for approval…";
        await _client.SignInWithDeviceAsync(authorization => _pending = authorization, null, _cancellation.Token);
    }
    catch (OperationCanceledException)
    {
        _status = "Sign-in cancelled.";
    }
    catch (Auth4DeviceAuthorizationException error)
    {
        _status = error.Reason == DeviceAuthorizationFailure.Denied ? "Sign-in was denied." : "The code expired.";
    }
    catch (Auth4Exception error)
    {
        _status = "Sign-in failed: " + error.Error;
    }
    finally
    {
        _pending = null;
        _cancellation.Dispose();
        _cancellation = null;
    }
}

Show UserCode and VerificationUri large enough to read from a couch. On platforms that can open a browser, VerificationUriComplete pre-fills the code. The default scope is openid offline_access. Polling, cancellation and expiry are covered in Device sign-in.

4. Guest play and upgrades

Guest sign-in and email upgrade
// Start playing straight away. guest.UserId is stable for the life of the account.
var guest = await client.SignInAsGuestAsync();

// Later, when the player wants to keep their progress:
var flow = await client.BeginGuestUpgradeAsync();          // valid for about five minutes
await client.StartEmailVerificationAsync(flow, email);       // sends a six-digit code
var proofId = await client.ConfirmEmailVerificationAsync(flow, email, code);
try
{
    var result = await client.CompleteGuestUpgradeAsync(flow, proofId);
    // result.Session.UserId == guest.UserId. The session and refresh token were replaced.
}
catch (Auth4Exception error) when (error.Error == "identity_in_use")
{
    // The email already belongs to another player. Nothing changed and nothing was merged:
    // offer to keep playing as a guest, or sign out and sign in to that account.
}
  • Sign-in methods refuse to replace an active session, so a guest is never abandoned silently. Upgrade the guest, or call SignOutAsync first.
  • Your project needs Guest accounts and Email verification codes turned on.
  • Read Guest accounts before shipping guest play.

5. Call your game server

Attaching the access token
try
{
    var accessToken = await client.GetAccessTokenAsync(); // refreshes shortly before expiry
    request.SetRequestHeader("Authorization", "Bearer " + accessToken);
}
catch (Auth4SessionExpiredException)
{
    // The refresh token expired or was revoked. The session is already cleared: sign in again.
}
catch (Auth4Exception error) when (error.IsTransient)
{
    // Network or server problem. The session is kept: retry later.
}

Concurrent calls share one refresh request, so parallel requests never trigger refresh-token reuse detection. Treat the token as opaque in the game and verify it on your server with the server SDK, using the game application's client ID as the audience.

SignOutAsync revokes the refresh token and always clears local state. It returns false if the server couldn't confirm revocation, for example when the player is offline.

6. Keep players signed in across restarts (optional)

By default the refresh token lives only in memory, so players sign in again after restarting the game. To persist sessions, implement ISecureStorageAdapter over your platform's protected store, such as the iOS Keychain, Android Keystore-backed encryption, Windows DPAPI or a console's protected save API:

Excerpt from packages/sdk-unity/Runtime/Auth4TokenStorage.cs
public interface ISecureStorageAdapter
{
    /// <summary>Returns the stored value, or null when the key is absent.</summary>
    Task<string> ReadAsync(string key, CancellationToken cancellationToken);

    Task WriteAsync(string key, string value, CancellationToken cancellationToken);

    Task DeleteAsync(string key, CancellationToken cancellationToken);
}
Restoring a session at start-up
var client = new Auth4Client(new Auth4ClientOptions
{
    TenantId = "ten_…",
    ClientId = "cli_…",
    AuthOrigin = "https://auth.auth4.dev",
    Transport = new UnityWebRequestTransport(),
    SecureStorage = new MyKeychainStorage(), // your ISecureStorageAdapter
});

if (!await client.TryRestoreSessionAsync())
{
    ShowSignInMenu();
}

Errors

ExceptionMeaning
Auth4DeviceAuthorizationExceptionReason is Denied (the player declined) or Expired (the code timed out). Offer to start again.
OperationCanceledExceptionYour cancellation token fired. A device authorization can be polled again until it expires.
Auth4SessionExpiredExceptionThe refresh token was rejected. The session and stored credentials are already cleared, andSessionChanged fired with null.
Auth4ExceptionAny other protocol error. Error holds the OAuth error code, andIsTransient is true for network failures and 5xx responses. Transient errors keep the session.
InvalidOperationExceptionA sign-in was started while a session is active, or an upgrade was attempted without a guest session.

Verify the package

The repository checks the package headlessly and in the Unity editor:

dotnet test packages/sdk-unity/Tests~/Auth4.Protocol.Tests/Auth4.Protocol.Tests.csproj
node packages/sdk-unity/Tests~/validate-package.mjs
./examples/unity/verify-unity.ps1 -UnityPath "C:\Program Files\Unity\Hub\Editor\6000.3.1f1\Editor\Unity.exe"

Sources: packages/sdk-unity/Tests~/Auth4.Protocol.Tests, packages/sdk-unity/Tests~/validate-package.mjs, examples/unity/verify-unity.ps1. Full package guide: packages/sdk-unity/Documentation~/README.md. Release notes: Unity changelog.