Sign in players
Sign in with a configured provider, use the player's Crossplay identity, and sign out.
Sign in
Configure your provider in Project Settings → Betide → Crossplay: EOS setup · Cognito setup.
For button-driven login, disable Auto Login On Start. For EOS, also set Auto Login Type to None.
Loading the interactive viewer.
Game module dependencies: UCIKCore, BetideCore.
#include "Engine/GameInstance.h"
#include "UCIKIdentitySubsystem.h"
DEFINE_LOG_CATEGORY_STATIC(LogGameIdentity, Log, All);
void SignInPlayer(UGameInstance *GameInstance)
{
if (!IsValid(GameInstance))
{
return;
}
UUCIKIdentitySubsystem *Identity = GameInstance->GetSubsystem<UUCIKIdentitySubsystem>();
if (!Identity || Identity->IsLoginInProgress())
{
return;
}
FUCIK_LoginRequest Request;
Request.LocalUserIndex = 0;
Request.MethodId = TEXT("Betide.EOS.Auth.AccountPortal");
Request.AccountPolicy = EUCIK_AccountPolicy::SignInOrCreate;
Identity->Login(Request,
FUCIKLoginUpdateCallback::CreateWeakLambda(
GameInstance, [](const FUCIK_LoginUpdate &Update) {
if (!Update.Operation.IsTerminal())
{
return;
}
if (Update.Operation.State == EUCIK_LoginOperationState::Succeeded)
{
UE_LOG(LogGameIdentity, Log, TEXT("User ID: %s"),
*Update.Identity.PrimaryHandle.Id);
UE_LOG(LogGameIdentity, Log, TEXT("User display name: %s"),
*Update.Identity.DisplayName);
return;
}
UE_LOG(LogGameIdentity, Warning, TEXT("Sign-in failed: %s"),
*Update.Error.GetLogString());
}));
}When updating a widget, bind the callback weakly to that widget. Login can complete synchronously.
Keep the full Player handle for other Crossplay calls. Prevent overlapping login attempts.
Sign out
Set Scope to Authority Session. An empty Authority uses the current identity. For EOS account switching, log out first.
Loading the interactive viewer.
Uses the includes and log category from the sign-in example.
void SignOutPlayer(UGameInstance *GameInstance)
{
if (!IsValid(GameInstance))
{
return;
}
UUCIKIdentitySubsystem *Identity = GameInstance->GetSubsystem<UUCIKIdentitySubsystem>();
if (!Identity || !Identity->GetIdentity().PrimaryHandle.IsValid())
{
return;
}
Identity->Logout({}, 0, EUCIK_LogoutScope::AuthoritySession,
FUCIKLogoutCallback::CreateWeakLambda(
GameInstance, [](const TBetideResult<FUCIK_LogoutResult> &Result) {
if (!Result.bOk)
{
UE_LOG(LogGameIdentity, Warning, TEXT("Sign-out failed: %s"),
*Result.Error.GetLogString());
return;
}
UE_LOG(LogGameIdentity, Log, TEXT("Signed out. Completed scope: %d"),
Result.GetValue().CompletedScope);
}));
}Logout clears UCIK’s cached identity. Completed Scope reports the provider’s completed actions; launcher/browser sign-out is not guaranteed.
More options
Automatic method selection
Auto uses the first eligible Auto Login Route for the platform, otherwise the available method with the highest priority. Configure routes under Show all in Crossplay settings. A failed login does not retry another route.
This method also works with button-driven login. In C++, set Request.MethodId = TEXT("Auto"); an empty method is invalid.
Startup login and cached identity
UCIK caches one identity per Game Instance across map changes. Local User Index does not create separate split-screen identity caches.
Leave Auto Login On Start enabled and call Start Observing Login when your menu opens. Do not also start login from the menu. This observer binds first, then reads the latest update. Stop Observing Login removes its binding without cancelling login; End Play also removes it.
Loading the interactive viewer.
Actor variables: ObservingLogin (Boolean, false) and LastObservedUpdate (Crossplay Login Update).
LoginObserver.h
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Identity/UCIKIdentityTypes.h"
#include "LoginObserver.generated.h"
class UUCIKIdentitySubsystem;
UCLASS()
class ALoginObserver : public AActor
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable) void StartObservingLogin();
UFUNCTION(BlueprintCallable) void StopObservingLogin();
UPROPERTY(BlueprintReadOnly) FUCIK_LoginUpdate LastObservedUpdate;
protected:
void EndPlay(const EEndPlayReason::Type Reason) override;
private:
TWeakObjectPtr<UUCIKIdentitySubsystem> Identity;
FDelegateHandle LoginBinding;
void HandleLoginUpdate(const FUCIK_LoginUpdate &Update);
};LoginObserver.cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
#include "LoginObserver.h"
#include "Engine/GameInstance.h"
#include "UCIKIdentitySubsystem.h"
#include "UCIKCore.h"
#include "UObject/Class.h"
void ALoginObserver::StartObservingLogin()
{
if (LoginBinding.IsValid())
return;
UGameInstance *Game = GetGameInstance();
Identity = Game ? Game->GetSubsystem<UUCIKIdentitySubsystem>() : nullptr;
if (!Identity.IsValid())
{
UE_LOG(LogUCIK, Warning, TEXT("Identity subsystem is unavailable"));
return;
}
LoginBinding =
Identity->OnLoginStateChangedNative.AddUObject(this, &ThisClass::HandleLoginUpdate);
HandleLoginUpdate(Identity->GetLastLoginUpdate());
}
void ALoginObserver::HandleLoginUpdate(const FUCIK_LoginUpdate &Update)
{
LastObservedUpdate = Update;
if (!Update.Operation.IsValid())
{
UE_LOG(LogUCIK, Log, TEXT("No recorded login operation; signed-out/reset state"));
return;
}
UE_LOG(LogUCIK, Log, TEXT("%s"), *UEnum::GetValueAsString(Update.Operation.State));
if (Update.Operation.State == EUCIK_LoginOperationState::Succeeded)
{
const FUCIK_PlayerHandle Current =
Identity.IsValid() ? Identity->GetIdentityHandle(Update.Identity.PrimaryAuthority)
: FUCIK_PlayerHandle{};
if (!Current.Id.IsEmpty() && Current.Id == Update.Identity.PrimaryHandle.Id)
UE_LOG(LogUCIK, Log, TEXT("%s"), *Update.Identity.DisplayName);
else
UE_LOG(LogUCIK, Log, TEXT("That login no longer matches the cached identity"));
return;
}
if (Update.Error.IsError())
{
UE_LOG(LogUCIK, Warning, TEXT("%s"), *Update.Error.GetLogString());
return;
}
if (Update.Operation.State == EUCIK_LoginOperationState::NeedsChallengeResponse ||
Update.Operation.State == EUCIK_LoginOperationState::NeedsUserInterface)
UE_LOG(LogUCIK, Log, TEXT("%s"), *Update.Challenge.Prompt.ToString());
}
void ALoginObserver::StopObservingLogin()
{
if (Identity.IsValid())
Identity->OnLoginStateChangedNative.Remove(LoginBinding);
LoginBinding.Reset();
Identity.Reset();
}
void ALoginObserver::EndPlay(const EEndPlayReason::Type Reason)
{
StopObservingLogin();
Super::EndPlay(Reason);
}Login challenges
This example signs in with Cognito SRP and answers its MFA challenge. Configure the User Pool first and disable Auto Login On Start for this button-driven flow.
Loading the interactive viewer.
Game module dependencies: UCIKCore, BetideCore, Engine. Use this Game Instance class, or merge its members into your own.
// CognitoLoginGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "UCIKIdentitySubsystem.h"
#include "CognitoLoginGameInstance.generated.h"
UCLASS()
class UCognitoLoginGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void SignInCognito(const FString &Username, const FString &Password);
UFUNCTION(BlueprintCallable)
void AnswerCognitoChallenge(const FString &Answer);
UFUNCTION(BlueprintCallable)
void CancelCognitoChallenge();
UPROPERTY(BlueprintReadOnly)
FUCIK_LoginUpdate CognitoLoginUpdate;
UPROPERTY(BlueprintReadOnly)
bool CognitoChallengePending = false;
};
// CognitoLoginGameInstance.cpp
#include "CognitoLoginGameInstance.h"
#include "UCIKCore.h"
void UCognitoLoginGameInstance::SignInCognito(const FString &Username, const FString &Password)
{
UUCIKIdentitySubsystem *Identity = GetSubsystem<UUCIKIdentitySubsystem>();
if (!Identity || Identity->IsLoginInProgress())
{
UE_LOG(LogUCIK, Warning, TEXT("Identity unavailable or a login is already in progress"));
return;
}
FUCIK_LoginRequest Request;
Request.LocalUserIndex = 0;
Request.MethodId = TEXT("Betide.Cognito.UserPool.SRP");
Request.Fields.FindOrAdd(TEXT("Username")).StringValue = Username;
Request.Fields.FindOrAdd(TEXT("Password")).StringValue = Password;
Identity->Login(
Request,
FUCIKLoginUpdateCallback::CreateWeakLambda(this, [this](const FUCIK_LoginUpdate &Update) {
CognitoLoginUpdate = Update;
CognitoChallengePending =
Update.Operation.State == EUCIK_LoginOperationState::NeedsChallengeResponse;
if (CognitoChallengePending)
{
UE_LOG(LogUCIK, Log, TEXT("%s"), *Update.Challenge.Prompt.ToString());
}
else if (Update.Operation.IsTerminal())
{
if (Update.Operation.State == EUCIK_LoginOperationState::Succeeded)
{
UE_LOG(LogUCIK, Log, TEXT("Signed in to Cognito"));
}
else
{
UE_LOG(LogUCIK, Warning, TEXT("%s"), *Update.Error.Message);
}
}
}));
}
void UCognitoLoginGameInstance::AnswerCognitoChallenge(const FString &Answer)
{
if (!CognitoChallengePending)
{
UE_LOG(LogUCIK, Warning, TEXT("No Cognito challenge is waiting for an answer"));
return;
}
UUCIKIdentitySubsystem *Identity = GetSubsystem<UUCIKIdentitySubsystem>();
if (!Identity)
{
return;
}
TMap<FName, FUCIK_LoginFieldValue> Responses;
Responses.FindOrAdd(CognitoLoginUpdate.Challenge.ResponseFields[0].FieldId).StringValue =
Answer;
CognitoChallengePending = false;
Identity->ContinueLogin(CognitoLoginUpdate.Operation, Responses);
}
void UCognitoLoginGameInstance::CancelCognitoChallenge()
{
if (!CognitoChallengePending)
{
UE_LOG(LogUCIK, Warning, TEXT("No Cognito challenge is waiting for an answer"));
return;
}
if (UUCIKIdentitySubsystem *Identity = GetSubsystem<UUCIKIdentitySubsystem>())
{
CognitoChallengePending = false;
Identity->CancelLogin(CognitoLoginUpdate.Operation);
}
}Keep this flow in your Game Instance. Required variables: CognitoLoginUpdate (Crossplay Login Update) and CognitoChallengePending (Boolean, false). Route login requests through this flow so its state listener receives only the active attempt.
Show the challenge prompt in your UI, then pass the player’s answer to Answer Cognito Challenge. Cancel Cognito Challenge cancels while the prompt is waiting. Neither event starts another login. Keep passwords and answers out of logs.
This Cognito flow uses one string response field. Other methods can request several fields or a different type; use their Response Fields descriptors. For browser redirects, see Cognito managed login.
Runtime methods and credentials
Get Crossplay Login Methods (GetLoginMethods in C++) returns method descriptors. Supply the platform and filter out unconfigured/unavailable methods. Use Runtime Login Request from the node’s context menu to supply the chosen Method Id and Fields; C++ uses FUCIK_LoginRequest.
UCIK normally acquires credentials. Enable Has Credential Override only when supplying a credential matching the method’s source, kind, and audience. Secret inputs must be runtime values, not literal pin defaults.
Troubleshooting
-
Method unavailable: check provider configuration and target-platform support.
-
Account Portal completes without opening: EOS reuses an existing login. Log out to switch accounts.
-
Provider error: use Get Error Message (Crossplay Error) and the error’s native code for diagnostics; C++ exposes
Error.GetLogString(). Use Get Error Text (Crossplay Error) for player-facing UI.