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. Tick
offline_accessunder 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:
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:
packages/sdk-unity/Samples~/DeviceLogin/DeviceLoginSample.csprivate 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
// 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
SignOutAsyncfirst. - Your project needs Guest accounts and Email verification codes turned on.
- Read Guest accounts before shipping guest play.
5. Call your game server
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:
packages/sdk-unity/Runtime/Auth4TokenStorage.cspublic 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);
}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
| Exception | Meaning |
|---|---|
Auth4DeviceAuthorizationException | Reason is Denied (the player declined) or Expired (the code timed out). Offer to start again. |
OperationCanceledException | Your cancellation token fired. A device authorization can be polled again until it expires. |
Auth4SessionExpiredException | The refresh token was rejected. The session and stored credentials are already cleared, andSessionChanged fired with null. |
Auth4Exception | Any other protocol error. Error holds the OAuth error code, andIsTransient is true for network failures and 5xx responses. Transient errors keep the session. |
InvalidOperationException | A 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.