Unreal Engine quickstart

The Unreal plugin brings the Unity package's flows to Unreal Engine 5. Players approve a short code on their phone or PC, or start as guests and link an email address or social account later. Use it from C++ or from Blueprint nodes.

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.
  • An Unreal Engine 5 C++ project. Blueprint-only projects need converting to C++ first.

1. Add the plugin

The plugin lives in packages/sdk-unreal. Copy or link that directory toPlugins/Auth4 in your project, regenerate project files and build. It has two runtime modules: Auth4Core, the protocol in plain C++, and Auth4, which adds settings, a game instance subsystem and Blueprint nodes. Add "Auth4" and"Auth4Core" to your module's dependencies to call it from C++.

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

2. Configure the client

Set the tenant ID, client ID and auth origin under Project Settings > Plugins > auth4.dev, or in Config/DefaultGame.ini. These are public values:

Excerpt from examples/unreal/Config/DefaultGame.ini
; Fill these in from the console (Applications > your native client), or use
; Project Settings > Plugins > auth4.dev. Native client IDs are public values.
[/Script/Auth4.Auth4Settings]
TenantId=
ClientId=
AuthOrigin=https://auth.auth4.dev
DefaultScope=openid offline_access

AuthOrigin is the origin of your issuer, without /t/…. HTTP is only accepted for loopback addresses during local development. UAuth4Subsystem creates the client on first use and owns it for the game instance. Requests go through Unreal's HTTP module, and every callback runs on the game thread.

3. Sign in with another device

In Blueprint, use Sign In With Device. On User Code fires with the code to show, then On Success or On Failure fires once. In C++,SignInWithDevice does the same. From the example project:

Excerpt from examples/unreal/Source/Auth4Example/Auth4ExamplePlayerController.cpp
void AAuth4ExamplePlayerController::Auth4DeviceSignIn()
{
	UAuth4Subsystem* Subsystem = UAuth4Subsystem::Get(this);
	Auth4::Error Error;
	const auto Client = Subsystem ? Subsystem->GetClient(Error) : nullptr;
	if (!Client)
	{
		Show(Describe(Error));
		return;
	}

	TWeakObjectPtr<AAuth4ExamplePlayerController> WeakThis(this);
	Client->SignInWithDevice(
		[WeakThis](const Auth4::DeviceAuthorization& Authorization)
		{
			if (AAuth4ExamplePlayerController* Self = WeakThis.Get())
			{
				Self->Show(FString::Printf(
					TEXT("Go to %s and enter %s"),
					*Auth4Unreal::ToFString(Authorization.VerificationUri),
					*Auth4Unreal::ToFString(Authorization.UserCode)));
			}
		},
		"",
		Subsystem->OperationToken(),
		[WeakThis](const Auth4::Result<Auth4::Session>& Result)
		{
			if (AAuth4ExamplePlayerController* Self = WeakThis.Get())
			{
				Self->Show(Result.Ok() ? DescribeSession(Result.Value()) : Describe(Result.GetError()));
			}
		});
}

Show UserCode and VerificationUri large enough to read from a couch.VerificationUriComplete pre-fills the code and suits a QR code. Polling waits the server's interval and slows down when asked. The code length, lifetime and interval come from the application'sdevice settings. Cancelling with CancelPendingOperations ends the wait with Cancelled.

4. Guest play and upgrades

Sign In As Guest (SignInAsGuest) starts playing at once. To keep a guest's progress, Upgrade Guest (UpgradeGuest) opens the hosted page in the system browser, where the player links an email address or social account, and polls until they finish. Set Preferred Provider to Google, Apple, Microsoft, Epic, Steam or Discord for a provider-first button; the other enabled methods stay available. If the chosen provider is off, the hosted page keeps its usual methods.

Saving guest progressExcerpt from examples/unreal/Source/Auth4Example/Auth4ExamplePlayerController.cpp
void AAuth4ExamplePlayerController::Auth4SaveProgress(const FString& Provider)
{
	UAuth4Subsystem* Subsystem = UAuth4Subsystem::Get(this);
	Auth4::Error Error;
	const auto Client = Subsystem ? Subsystem->GetClient(Error) : nullptr;
	if (!Client)
	{
		Show(Describe(Error));
		return;
	}

	std::optional<Auth4::Provider> Hint;
	if (Provider.Equals(TEXT("steam"), ESearchCase::IgnoreCase))
	{
		Hint = Auth4::Provider::Steam;
	}
	else if (Provider.Equals(TEXT("discord"), ESearchCase::IgnoreCase))
	{
		Hint = Auth4::Provider::Discord;
	}
	else if (Provider.Equals(TEXT("epic"), ESearchCase::IgnoreCase))
	{
		Hint = Auth4::Provider::Epic;
	}
	else if (Provider.Equals(TEXT("google"), ESearchCase::IgnoreCase))
	{
		Hint = Auth4::Provider::Google;
	}
	else if (Provider.Equals(TEXT("apple"), ESearchCase::IgnoreCase))
	{
		Hint = Auth4::Provider::Apple;
	}
	else if (Provider.Equals(TEXT("microsoft"), ESearchCase::IgnoreCase))
	{
		Hint = Auth4::Provider::Microsoft;
	}

	TWeakObjectPtr<AAuth4ExamplePlayerController> WeakThis(this);
	Client->UpgradeGuest(
		Hint,
		[WeakThis](const Auth4::UpgradeProgress& Progress)
		{
			AAuth4ExamplePlayerController* Self = WeakThis.Get();
			if (Self != nullptr && Progress.Stage == Auth4::UpgradeStage::OpeningBrowser)
			{
				Self->Show(TEXT("Finish in your browser. Type Auth4Cancel to stop waiting."));
			}
		},
		Subsystem->OperationToken(),
		[WeakThis](const Auth4::Result<Auth4::UpgradeResult>& Result)
		{
			if (AAuth4ExamplePlayerController* Self = WeakThis.Get())
			{
				Self->Show(Result.Ok()
				               ? FString::Printf(TEXT("Progress saved with %s."), *Auth4Unreal::ToFString(Result.Value().Identity.Provider))
				               : Describe(Result.GetError()));
			}
		});
}
  • On success the tokens are replaced in one step and the user ID doesn't change. If the account already belongs to another player, the node fails with UpgradeConflict; nothing is merged and the guest keeps playing.
  • The page link is safe to show again from On Progress: it holds only a public handle. The secret that collects the tokens never leaves the game.
  • If you cancel after the player has already finished in the browser, the account is still upgraded. The guest session is ended, so the player signs in with the account they linked.
  • Sign-in nodes refuse to replace an active session, so a guest is never abandoned silently. Upgrade the guest, or sign out first.

Read Guest accounts before shipping guest play.

5. Call your game server

Get Access Token (GetAccessToken) returns a valid access token and refreshes it first when it is about to expire. Concurrent calls share one refresh request, so parallel requests never trigger refresh-token reuse detection.

Attaching the access token
Client->GetAccessToken([this](const Auth4::Result<std::string>& Token)
{
    if (Token.Ok())
    {
        // Send it only in a header. Never print, log or save it.
        Request->SetHeader(TEXT("Authorization"), TEXT("Bearer ") + Auth4Unreal::ToFString(Token.Value()));
        Request->ProcessRequest();
    }
    else if (Token.GetError().Kind == Auth4::ErrorKind::SessionExpired)
    {
        ShowSignInMenu(); // the session is already cleared
    }
    else if (Token.GetError().IsTransient())
    {
        RetryLater(); // network or server problem; the session is kept
    }
});

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. Sign Out revokes the refresh token and always clears local state. Its bRevoked output is false when the server couldn't confirm, for example when the player is offline.

Bind OnSessionChanged on the subsystem to update menus after any sign-in, refresh, upgrade or sign-out.

6. Keep players signed in across restarts (optional)

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

Excerpt from packages/sdk-unreal/Source/Auth4/Public/Auth4SecureStorage.h
class AUTH4_API IAuth4SecureStorage
{
public:
	virtual ~IAuth4SecureStorage() = default;

	/** bFound is false when the key is absent; bOk is false when storage is unavailable. */
	virtual void Read(const FString& Key, TFunction<void(bool bOk, bool bFound, const FString& Value)> Done) = 0;

	virtual void Write(const FString& Key, const FString& Value, TFunction<void(bool bOk)> Done) = 0;

	virtual void Delete(const FString& Key, TFunction<void(bool bOk)> Done) = 0;
};
Restoring a session at start-up
// In your GameInstance, before the first auth4.dev call:
UAuth4Subsystem* Auth4 = GetSubsystem<UAuth4Subsystem>();
Auth4->SetSecureStorage(MakeShared<FMyKeychainStorage>()); // your IAuth4SecureStorage

Auth4::Error Error;
if (auto Client = Auth4->GetClient(Error))
{
    Client->TryRestoreSession([this](const Auth4::Result<bool>& Restored)
    {
        if (!Restored.Ok() || !Restored.Value())
        {
            ShowSignInMenu();
        }
    });
}

The Restore Session node does the same from Blueprint.

Errors

Failure pins carry an FAuth4Error. Branch on Kind and show your own wording; Code is for logs.

KindMeaning
DeviceDenied, DeviceExpiredThe player declined, or the code timed out. Offer to start again.
UpgradeConflict, UpgradeDenied, UpgradeExpiredThe upgrade ended without linking: the account belongs to another player, the player choseNot now, or the page link expired. The guest session is unchanged.
CancelledCancelPendingOperations ran. A device authorization can be polled again from C++ until it expires.
SessionExpiredThe refresh token was rejected. The session and stored credentials are already cleared, and OnSessionChanged fired with bSignedIn false.
LoginRequired, InvalidStateNo session is active, a sign-in was started while one is, or an upgrade was attempted without a guest session.
InvalidArgumentThe project settings are missing or invalid.
NotSupportedThe platform has no system browser, so guests can't be upgraded there.
Network, Protocol, InvalidResponseOther failures. bIsTransient is true for network failures and 5xx responses; transient errors keep the session.

Verify the plugin

The protocol core builds without the engine. This command compiles it with CMake (warnings as errors, exceptions off, as in Unreal) and runs the tests against a local mock server:

node packages/sdk-unreal/Tests/run-tests.mjs

With Unreal Engine 5 installed, this command packages the plugin with BuildPlugin, builds the example project with it and runs the Auth4.* automation tests headless, including sign-in, refresh and sign-out through Unreal's HTTP module against the mock server. The auth4.dev release checks run it on Unreal Engine 5.7 for Windows.

node packages/sdk-unreal/Tests/run-engine-tests.mjs --engine <UE_5.x directory>

In the editor, the same tests are under Tools > Session Frontend > Automation, filtered by Auth4.

Sources: packages/sdk-unreal/Tests, examples/unreal. Plugin notes: packages/sdk-unreal/README.md.