Skip to content

Instantly share code, notes, and snippets.

@nullbus
Created January 8, 2026 13:29
Show Gist options
  • Select an option

  • Save nullbus/d2ad6ff6cc5360c56c8dbfb7d9b94fbe to your computer and use it in GitHub Desktop.

Select an option

Save nullbus/d2ad6ff6cc5360c56c8dbfb7d9b94fbe to your computer and use it in GitHub Desktop.
EOS token retrieval example using Steamworks auth ticket

[UE5] EOS token retrieval example using Steamworks auth ticket

This example shows concepts below:

  • [UE5] how to authenticate EOS with external auth token. For this case, Steamworks auth ticket is used.
  • [UE5] how to generate EOS JWT
  • [Typescript] how to verify EOS JWT

To verify JWT, you need to install extra typescript libraries:

npm install jsonwebtoken jwks-rsa @types/jsonwebtoken @types/jwks-rsa
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
THIRD_PARTY_INCLUDES_START
#include "steam/steam_api.h"
THIRD_PARTY_INCLUDES_END
#include "LoginScene.generated.h"
class UButton;
class UVerticalBox;
namespace UE::Online
{
template <typename T>
class TOnlineResult;
struct FAuthLogin;
}
UCLASS(Abstract, BlueprintType)
class STEAMEOS_API ULoginScene : public UUserWidget
{
GENERATED_BODY()
protected:
UPROPERTY(meta = (BindWidget))
UButton* BTN_Login;
UPROPERTY(meta = (BindWidget))
UVerticalBox* VB_Log;
bool bSteamEnabled = false;
HAuthTicket AuthTicketHandle = 0;
CCallbackManual<ULoginScene, GetTicketForWebApiResponse_t> CallbackGetTicketForWebApi;
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
protected:
UFUNCTION()
void OnLoginClicked();
void OnGetAuthTicketForWebApiCompleted(struct GetTicketForWebApiResponse_t* Response);
void StartLoginEOS(const FString& SteamTicket);
void OnEOSLoginCompleted(const UE::Online::TOnlineResult<UE::Online::FAuthLogin>& Result);
void AddToLog(ELogVerbosity::Type Verbosity, const FString& Message);
};
#include "LoginScene.h"
#include "Components/Button.h"
#include "Components/VerticalBox.h"
#include "Components/VerticalBoxSlot.h"
#include "Components/TextBlock.h"
#include "Online/OnlineServices.h"
#include "Online/Auth.h"
#include "Online/OnlineAsyncOp.h"
#include "Online/OnlineIdEOSGS.h"
#include "IEOSSDKManager.h"
#include "EOSShared.h"
THIRD_PARTY_INCLUDES_START
#include "eos_sdk.h"
THIRD_PARTY_INCLUDES_END
DEFINE_LOG_CATEGORY_STATIC(LogSteamEOS, Log, All);
#define EOSLOG(Verbosity, Message, ...) AddToLog(ELogVerbosity::Verbosity, FString::Printf(Message, ##__VA_ARGS__));
void ULoginScene::NativeConstruct()
{
Super::NativeConstruct();
BTN_Login->OnClicked.AddDynamic(this, &ULoginScene::OnLoginClicked);
EOSLOG(Log, TEXT("LoginScene ready"));
// Already done by 'SteamShared' module, just for check if Steamworks is usable
bSteamEnabled = SteamAPI_Init();
EOSLOG(Log, TEXT("Steam enabled: %s"), *LexToString(bSteamEnabled));
}
void ULoginScene::NativeDestruct()
{
CallbackGetTicketForWebApi.Unregister();
// Processed by 'SteamShared', would't do here
// SteamAPI_Shutdown();
Super::NativeDestruct();
}
void ULoginScene::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
// Pump Steamworks API Callbacks
SteamAPI_RunCallbacks();
}
void ULoginScene::OnLoginClicked()
{
if (!bSteamEnabled)
{
EOSLOG(Error, TEXT("Steam is not running"));
return;
}
if (AuthTicketHandle != 0)
{
EOSLOG(Error, TEXT("Previous request is not completed"));
return;
}
// Register callback
CallbackGetTicketForWebApi.Register(this, &ULoginScene::OnGetAuthTicketForWebApiCompleted);
constexpr char ApiTarget[] = "epiconlineservices";
AuthTicketHandle = SteamUser()->GetAuthTicketForWebApi(ApiTarget);
EOSLOG(Log, TEXT("Login clicked, Steam auth ticket handle: 0x%08X"), AuthTicketHandle);
}
void ULoginScene::OnGetAuthTicketForWebApiCompleted(GetTicketForWebApiResponse_t* Response)
{
if (Response->m_hAuthTicket != AuthTicketHandle)
{
EOSLOG(Error, TEXT("Auth ticket handle mismatch: 0x%08X != 0x%08X"), Response->m_hAuthTicket, AuthTicketHandle);
return;
}
AuthTicketHandle = 0;
if (Response->m_eResult != k_EResultOK)
{
EOSLOG(Error, TEXT("Failed to get auth ticket for web api: %d"), Response->m_eResult);
return;
}
FString TokenString = FString::FromHexBlob(Response->m_rgubTicket, Response->m_cubTicket);
EOSLOG(Log, TEXT("Steam Auth ticket for web api received: %s"), *TokenString);
StartLoginEOS(TokenString);
}
void ULoginScene::StartLoginEOS(const FString& SteamTicket)
{
EOSLOG(Log, TEXT("Starting EOS login with Steam ticket: %s"), *SteamTicket);
UE::Online::IOnlineServicesPtr OnlineServices = UE::Online::GetServices(UE::Online::EOnlineServices::Epic);
// UE::Online::IOnlineServicesPtr OnlineServices = UE::Online::GetServices();
if (!OnlineServices.IsValid())
{
EOSLOG(Error, TEXT("OnlineServices is not valid"));
return;
}
UE::Online::IAuthPtr AuthInterface = OnlineServices->GetAuthInterface();
if (!AuthInterface.IsValid())
{
EOSLOG(Error, TEXT("AuthInterface is not valid"));
return;
}
ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (!LocalPlayer)
{
EOSLOG(Error, TEXT("No local player found"));
return;
}
UE::Online::FAuthLogin::Params LoginParams;
LoginParams.PlatformUserId = LocalPlayer->GetPlatformUserId();
LoginParams.CredentialsType = UE::Online::LoginCredentialsType::ExternalAuth;
LoginParams.CredentialsToken.Set<UE::Online::FExternalAuthToken>(
{
.Type = UE::Online::ExternalLoginType::SteamSessionTicket,
.Data = SteamTicket,
});
AuthInterface->Login(MoveTemp(LoginParams)).OnComplete(this, &ULoginScene::OnEOSLoginCompleted);
}
void ULoginScene::OnEOSLoginCompleted(const UE::Online::TOnlineResult<UE::Online::FAuthLogin>& Result)
{
if (!Result.IsOk())
{
EOSLOG(Error, TEXT("EOS login failed: %s"), *Result.GetErrorValue().GetText().ToString());
return;
}
const static FString TargetPlatformName = TEXT("OnlineServices.EOS");
IEOSPlatformHandlePtr EpicPlatform;
for (const IEOSPlatformHandlePtr& ActivePlatform : IEOSSDKManager::Get()->GetActivePlatforms())
{
if (ActivePlatform->GetConfigName() == TargetPlatformName)
{
EpicPlatform = ActivePlatform;
break;
}
}
if (!EpicPlatform.IsValid())
{
EOSLOG(Error, TEXT("Target platform not found: %s"), *TargetPlatformName);
return;
}
EOS_HPlatform EpicPlatformHandle = *EpicPlatform;
if (EpicPlatformHandle == nullptr)
{
EOSLOG(Error, TEXT("Epic platform handle is null"));
return;
}
const UE::Online::FAccountId AccountId = Result.GetOkValue().AccountInfo->AccountId;
EOS_HConnect ConnectHandle = EOS_Platform_GetConnectInterface(EpicPlatformHandle);
EOS_Connect_CopyIdTokenOptions CopyIdTokenOptions =
{
.ApiVersion = EOS_CONNECT_COPYIDTOKEN_API_LATEST,
.LocalUserId = UE::Online::GetProductUserIdChecked(AccountId),
};
EOS_Connect_IdToken* IdToken = nullptr;
EOS_EResult CopyIdTokenResult = EOS_Connect_CopyIdToken(ConnectHandle, &CopyIdTokenOptions, &IdToken);
if (CopyIdTokenResult != EOS_EResult::EOS_Success)
{
EOSLOG(Error, TEXT("Failed to copy id token: %s"), *LexToString(CopyIdTokenResult));
return;
}
// We got the token
// The token can be used later for further authentication on server side.
FString Token = UTF8_TO_TCHAR(IdToken->JsonWebToken);
EOSLOG(Log, TEXT("EOS login successful: %s"), *Token);
}
// Utility function for log visualization
void ULoginScene::AddToLog(ELogVerbosity::Type Verbosity, const FString& Message)
{
constexpr FLinearColor ColorLog = FLinearColor(1, 1, 1, 1);
constexpr FLinearColor ColorWarning = FLinearColor(1, 1, 0, 1);
constexpr FLinearColor ColorError = FLinearColor(1, 0, 0, 1);
float Time = GetWorld()->GetTimeSeconds();
FString FormattedMessage = FString::Printf(TEXT("%.2f: %s"), Time, *Message);
UTextBlock* TextBlock = NewObject<UTextBlock>(this);
TextBlock->SetText(FText::FromString(FormattedMessage));
switch (Verbosity)
{
case ELogVerbosity::Warning:
TextBlock->SetColorAndOpacity(ColorWarning);
break;
case ELogVerbosity::Error:
TextBlock->SetColorAndOpacity(ColorError);
break;
default:
TextBlock->SetColorAndOpacity(ColorLog);
break;
}
UVerticalBoxSlot* NewSlot = VB_Log->AddChildToVerticalBox(TextBlock);
NewSlot->SetVerticalAlignment(EVerticalAlignment::VAlign_Center);
NewSlot->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill);
NewSlot->SetPadding(FMargin(0, 0, 0, 0));
UE_LOG(LogSteamEOS, Log, TEXT("%s"), *Message);
}
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
// Define the EOS instance as a JWKS client
const eos = jwksClient({
jwksUri: 'https://api.epicgames.dev/epic/oauth/v2/.well-known/jwks.json',
cache: true, // Recommended for performance
rateLimit: true,
jwksRequestsPerMinute: 5
});
// Helper function to retrieve the signing key from the JWKS endpoint
const getKey: jwt.GetPublicKeyOrSecret = (header, callback) => {
client.getSigningKey(header.kid, (err, key) => {
if (err || !key) {
callback(err || new Error("Public key not found"));
} else {
const signingKey = key.getPublicKey();
callback(null, signingKey);
}
});
};
// validate EOS JWT and return ProductUserId
const validateEOSToken = async (client: jwksClient.JwksClient, tokenStr: string): Promise<string> => {
const token = await new Promise<jwt.JwtPayload>((resolve, reject) =>
jwt.verify(tokenStr, getKey, { complete: false }, (err, payload) => {
if (err) {
console.debug("toekn verification error:", err);
reject(err);
} else {
resolve(payload as jwt.JwtPayload);
}
})
);
const {
sub: eosId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
pfdid, // EOS Deployment ID
// eslint-disable-next-line @typescript-eslint/no-unused-vars
pfpid, // EOS Product ID
// eslint-disable-next-line @typescript-eslint/no-unused-vars
pfsid, // EOS Sandbox ID
} = token;
if (typeof eosId !== "string") {
console.debug("invalid eosId", JSON.stringify(token));
throw new Error("invalid eosId");
}
return eosId;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment