Skip to content

chore: PKCE for Windows support (alpha release) #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 5, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ void UImtblConnectionAsyncActions::DoConnect(TWeakObjectPtr<UImtblJSConnector> J
{
if (bIsPKCE)
{
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
Passport->ConnectPKCE(bIsConnectImx, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UImtblConnectionAsyncActions::OnConnect));
#endif
}
27 changes: 22 additions & 5 deletions Source/Immutable/Private/Immutable/ImmutableDataTypes.cpp
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@

#include "Immutable/ImmutableDataTypes.h"

#if PLATFORM_WINDOWS
#include "Immutable/Windows/ImmutablePKCEWindows.h"
#endif

FString FImmutablePassportInitData::ToJsonString() const
{
@@ -29,20 +32,20 @@ FString FImmutablePassportInitData::ToJsonString() const
TOptional<FImmutablePassportInitDeviceFlowData> FImmutablePassportInitDeviceFlowData::FromJsonString(const FString& JsonObjectString)
{
FImmutablePassportInitDeviceFlowData PassportConnect;

if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonObjectString, &PassportConnect, 0, 0))
{
IMTBL_WARN("Could not parse response from JavaScript into the expected " "Passport connect format")
return TOptional<FImmutablePassportInitDeviceFlowData>();
}

return PassportConnect;
}

FString FImmutablePassportZkEvmRequestAccountsData::ToJsonString() const
{
FString OutString;

FJsonObjectConverter::UStructToJsonObjectString(*this, OutString, 0, 0, 0, nullptr, false);

return OutString;
@@ -56,7 +59,7 @@ TOptional<FImmutablePassportZkEvmRequestAccountsData> FImmutablePassportZkEvmReq
IMTBL_WARN("Could not parse response from JavaScript into the expected " "Passport ZkEvm request accounts format")
return TOptional<FImmutablePassportZkEvmRequestAccountsData>();
}

return RequestAccounts;
}

@@ -71,7 +74,7 @@ TOptional<FImmutablePassportZkEvmRequestAccountsData> FImmutablePassportZkEvmReq
IMTBL_ERR("Could not parse response from JavaScript into the expected " "Passport ZkEvm request accounts format")
return TOptional<FImmutablePassportZkEvmRequestAccountsData>();
}

return RequestAccounts;
}

@@ -92,3 +95,17 @@ FString FImmutablePassportZkEvmGetBalanceData::ToJsonString() const

return OutString;
}

void UImmutablePKCEData::BeginDestroy()
{
Reset();

UObject::BeginDestroy();
}

void UImmutablePKCEData::Reset()
{
#if PLATFORM_WINDOWS
UImmutablePKCEWindows::Reset(this);
#endif
}
120 changes: 84 additions & 36 deletions Source/Immutable/Private/Immutable/ImmutablePassport.cpp
Original file line number Diff line number Diff line change
@@ -23,6 +23,9 @@
#include "GenericPlatform/GenericPlatformProcess.h"
#include "Mac/ImmutableMac.h"
#endif
#if PLATFORM_WINDOWS
#include "Immutable/Windows/ImmutablePKCEWindows.h"
#endif

#define PASSPORT_SAVE_GAME_SLOT_NAME TEXT("Immutable")

@@ -101,10 +104,22 @@ void UImmutablePassport::Connect(bool IsConnectImx, bool TryToRelogin, const FIm
}
}

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
void UImmutablePassport::ConnectPKCE(bool IsConnectImx, const FImtblPassportResponseDelegate& ResponseDelegate)
{
SetStateFlags(IPS_CONNECTING | IPS_PKCE);

#if PLATFORM_WINDOWS
// Verify PKCEData is null before initializing to ensure we're not overriding an active PKCE operation.
// A non-null value indicates another PKCE operation is already in progress.
ensureAlways(!PKCEData);
PKCEData = UImmutablePKCEWindows::Initialise(InitData);
if (PKCEData)
{
PKCEData->DynamicMulticastDelegate_DeepLinkCallback.AddDynamic(this, &ThisClass::OnDeepLinkActivated);
}
#endif

if (IsConnectImx)
{
SetStateFlags(IPS_IMX);
@@ -117,7 +132,17 @@ void UImmutablePassport::ConnectPKCE(bool IsConnectImx, const FImtblPassportResp

void UImmutablePassport::Logout(bool DoHardLogout, const FImtblPassportResponseDelegate& ResponseDelegate)
{
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_WINDOWS
// Verify PKCEData is null before initializing to ensure we're not overriding an active PKCE operation.
// A non-null value indicates another PKCE operation is already in progress.
ensureAlways(!PKCEData);
PKCEData = UImmutablePKCEWindows::Initialise(InitData);
if (PKCEData)
{
PKCEData->DynamicMulticastDelegate_DeepLinkCallback.AddDynamic(this, &ThisClass::OnDeepLinkActivated);
}
#endif
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
if (IsStateFlagsSet(IPS_PKCE))
{
PKCELogoutResponseDelegate = ResponseDelegate;
@@ -392,7 +417,7 @@ TOptional<UImmutablePassport::FImtblPassportResponseDelegate> UImmutablePassport

void UImmutablePassport::OnInitializeResponse(FImtblJSResponse Response)
{
if (auto ResponseDelegate = GetResponseDelegate(Response))
if (TOptional<FImtblPassportResponseDelegate> ResponseDelegate = GetResponseDelegate(Response))
{
FString Error;

@@ -470,43 +495,55 @@ void UImmutablePassport::OnLogoutResponse(FImtblJSResponse Response)

return;
}

FString Url;
FString ErrorMessage;

auto Logout = [this](const FImtblJSResponse& Response)
{
TOptional<FImtblPassportResponseDelegate> ResponseDelegate = GetResponseDelegate(Response);

FString Url;
Response.JsonObject->TryGetStringField(TEXT("result"), Url);

FString ErrorMessage;
FPlatformProcess::LaunchURL(*Url, nullptr, &ErrorMessage);

if (ErrorMessage.Len())
{
ErrorMessage = "Failed to launch browser: " + ErrorMessage;
IMTBL_ERR("%s", *ErrorMessage);
ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{false, ErrorMessage, Response});
}
};

ResetStateFlags(IPS_HARDLOGOUT);

FString Url;
Response.JsonObject->TryGetStringField(TEXT("result"), Url);

if (!Url.IsEmpty())
{
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
if (IsStateFlagsSet(IPS_PKCE))
{
OnHandleDeepLink = FImtblPassportHandleDeepLinkDelegate::CreateUObject(this, &UImmutablePassport::OnDeepLinkActivated);
OnHandleDeepLink.AddUObject(this, &UImmutablePassport::OnDeepLinkActivated);
#if PLATFORM_ANDROID
LaunchAndroidUrl(Url);
#elif PLATFORM_IOS
[[ImmutableIOS instance] launchUrl:TCHAR_TO_ANSI(*Url)];
#elif PLATFORM_MAC
[[ImmutableMac instance] launchUrl:TCHAR_TO_ANSI(*Url) forRedirectUri:TCHAR_TO_ANSI(*InitData.logoutRedirectUri)];
#endif
#if PLATFORM_WINDOWS
Logout(Response);
#endif
}
else
{
#endif
FPlatformProcess::LaunchURL(*Url, nullptr, &ErrorMessage);
if (ErrorMessage.Len())
{
Message = "Failed to connect to Browser: " + ErrorMessage;

IMTBL_ERR("%s", *Message);
ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{ false, Message, Response });

return;
}
Logout(Response);
Analytics->Track(UImmutableAnalytics::EEventName::COMPLETE_LOGOUT);
IMTBL_LOG("Logged out")
ResponseDelegate->ExecuteIfBound(FImmutablePassportResult{ Response.success });
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
}
#endif
}
@@ -517,7 +554,7 @@ void UImmutablePassport::OnLogoutResponse(FImtblJSResponse Response)
ResetStateFlags(IPS_CONNECTED);
}

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response)
{
if (PKCEResponseDelegate.IsBound())
@@ -532,7 +569,7 @@ void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response)
else
{
// Handle deeplink calls
OnHandleDeepLink = FImtblPassportHandleDeepLinkDelegate::CreateUObject(this, &UImmutablePassport::OnDeepLinkActivated);
OnHandleDeepLink.AddUObject(this, &UImmutablePassport::OnDeepLinkActivated);

Msg = Response.JsonObject->GetStringField(TEXT("result")).Replace(TEXT(" "), TEXT("+"));
#if PLATFORM_ANDROID
@@ -542,6 +579,17 @@ void UImmutablePassport::OnGetPKCEAuthUrlResponse(FImtblJSResponse Response)
[[ImmutableIOS instance] launchUrl:TCHAR_TO_ANSI(*Msg)];
#elif PLATFORM_MAC
[[ImmutableMac instance] launchUrl:TCHAR_TO_ANSI(*Msg) forRedirectUri:TCHAR_TO_ANSI(*InitData.redirectUri)];
#elif PLATFORM_WINDOWS
FString ErrorMessage;
FPlatformProcess::LaunchURL(*Msg, nullptr, &ErrorMessage);
if (!ErrorMessage.IsEmpty())
{
ErrorMessage = "Failed to launch browser: " + ErrorMessage;
IMTBL_ERR("%s", *ErrorMessage);
PKCEResponseDelegate.ExecuteIfBound(FImmutablePassportResult{false, ErrorMessage});
PKCEResponseDelegate.Unbind();
ResetStateFlags(IPS_PKCE | IPS_CONNECTING);
}
#endif
}
}
@@ -583,7 +631,6 @@ void UImmutablePassport::OnConnectPKCEResponse(FImtblJSResponse Response)
}
ResetStateFlags(IPS_COMPLETING_PKCE);
}
#endif

void UImmutablePassport::OnConfirmCodeResponse(FImtblJSResponse Response)
{
@@ -666,11 +713,9 @@ void UImmutablePassport::LoadPassportSettings()
}
}

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
void UImmutablePassport::OnDeepLinkActivated(FString DeepLink)
void UImmutablePassport::OnDeepLinkActivated(const FString& DeepLink)
{
IMTBL_LOG_FUNC("URL : %s", *DeepLink);
OnHandleDeepLink = nullptr;
OnHandleDeepLink.Clear();
if (DeepLink.StartsWith(InitData.logoutRedirectUri))
{
// execute on game thread to prevent call to Passport instance from another thread
@@ -690,6 +735,8 @@ void UImmutablePassport::OnDeepLinkActivated(FString DeepLink)
{
CompleteLoginPKCEFlow(DeepLink);
}

PKCEData = nullptr;
}

void UImmutablePassport::CompleteLoginPKCEFlow(FString Url)
@@ -739,30 +786,31 @@ void UImmutablePassport::CompleteLoginPKCEFlow(FString Url)
FImtblJSResponseDelegate::CreateUObject(this, &UImmutablePassport::OnConnectPKCEResponse));
}
}

#endif

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID
#if PLATFORM_ANDROID | PLATFORM_WINDOWS
// Called from Android JNI
void UImmutablePassport::HandleDeepLink(FString DeepLink) const
{
#elif PLATFORM_IOS | PLATFORM_MAC

#endif
#if PLATFORM_IOS | PLATFORM_MAC
// Called from iOS Objective C
void UImmutablePassport::HandleDeepLink(NSString* sDeepLink) const
#endif
{
#if PLATFORM_IOS | PLATFORM_MAC
FString DeepLink = FString(UTF8_TO_TCHAR([sDeepLink UTF8String]));
IMTBL_LOG("Handle Deep Link: %s", *DeepLink);
#endif

if (!OnHandleDeepLink.ExecuteIfBound(DeepLink))
#if PLATFORM_WINDOWS
if (PKCEData)
{
IMTBL_WARN("OnHandleDeepLink delegate was not called");
UImmutablePKCEWindows::HandleDeepLink(PKCEData, DeepLink);
}
}
#endif

OnHandleDeepLink.Broadcast(DeepLink);
}

#if PLATFORM_ANDROID
void UImmutablePassport::HandleOnLoginPKCEDismissed()
{
703 changes: 703 additions & 0 deletions Source/Immutable/Private/Immutable/Windows/ImmutablePKCEWindows.cpp

Large diffs are not rendered by default.

44 changes: 42 additions & 2 deletions Source/Immutable/Public/Immutable/ImmutableDataTypes.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#pragma once

#include "JsonObjectConverter.h"
#include "Misc/EngineVersion.h"

#include "Immutable/ImtblJSMessages.h"
#include "Immutable/ImmutableNames.h"

#include "ImmutableDataTypes.generated.h"

DECLARE_MULTICAST_DELEGATE_OneParam(FImmutableDeepLinkMulticastDelegate, const FString& /** DeepLink */);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FImmutableDeepLinkDynamicMulticastDelegate, const FString&, DeepLink);

// This is the version of the Unreal Immutable SDK that is being used. This is not the version of the engine.
// This hardcoded value will be updated by a workflow during the release process.
#define ENGINE_SDK_VERSION TEXT("1.9.0")
@@ -43,7 +45,7 @@ struct FImmutableEngineVersionData
/**
* Structure to hold initialisation data for the Immutable Passport.
*/
USTRUCT()
USTRUCT(BlueprintType)
struct IMMUTABLE_API FImmutablePassportInitData
{
GENERATED_BODY()
@@ -302,4 +304,42 @@ struct IMMUTABLE_API FZkEvmTransactionReceipt

UPROPERTY()
FString type;
};

/**
* Data for PKCE deep linking
*/
UCLASS(BlueprintType, DisplayName = "Immutable PKCE Data")
class IMMUTABLE_API UImmutablePKCEData : public UObject
{
GENERATED_BODY()

public:
/** UObject: @Interface @Begin */
virtual void BeginDestroy() override;
/** UObject: @Interface @End */

/**
* Reset and clean up PKCE operation footprint
* Automatically called during object destruction
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|Passport")
void Reset();

public:
/** Passport initialization data */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FImmutablePassportInitData PassportInitData;

/**
* Delegate triggered when a deep link callback is received from the browser
* Contains the complete URI with authorization code and state parameters
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, BlueprintAssignable, DisplayName = "Deep Link Callback")
FImmutableDeepLinkDynamicMulticastDelegate DynamicMulticastDelegate_DeepLinkCallback;

/**
* Handle for the ticker delegate that periodically checks for incoming deep links
*/
FTSTicker::FDelegateHandle TickDelegateHandle;
};
2 changes: 1 addition & 1 deletion Source/Immutable/Public/Immutable/ImmutableNames.h
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ namespace ImmutablePassportAction
const FString ZkEvmGetTransactionReceipt = TEXT("zkEvmGetTransactionReceipt");
const FString ZkEvmSignTypedDataV4 = TEXT("zkEvmSignTypedDataV4");

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
const FString GetPKCEAuthUrl = TEXT("getPKCEAuthUrl");
const FString LOGIN_PKCE = TEXT("loginPKCE");
const FString CONNECT_PKCE = TEXT("connectPKCE");
91 changes: 50 additions & 41 deletions Source/Immutable/Public/Immutable/ImmutablePassport.h
Original file line number Diff line number Diff line change
@@ -72,6 +72,13 @@ class IMMUTABLE_API UImmutablePassport : public UObject
*/
DECLARE_DELEGATE_OneParam(FImtblPassportResponseDelegate, FImmutablePassportResult);

#if PLATFORM_ANDROID
/**
* Delegate used for handling the dismissal of the PKCE flow on Android.
*/
DECLARE_DELEGATE(FImtblPassportOnPKCEDismissedDelegate);
#endif

/**
* Initialises passport. This sets up the Passport instance, configures the web browser, and waits for the ready signal.
*
@@ -99,9 +106,9 @@ class IMMUTABLE_API UImmutablePassport : public UObject
* @param ResponseDelegate Callback delegate.
*/
void Connect(bool IsConnectImx, bool TryToRelogin, const FImtblPassportResponseDelegate& ResponseDelegate);
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
/**
* (Android, iOS and macOS only) Logs into Passport using Authorisation Code Flow with Proof Key for Code Exchange (PKCE)
* Logs into Passport using Authorisation Code Flow with Proof Key for Code Exchange (PKCE)
*
* @param IsConnectImx If true, player will go through the device code auth login flow and connect to Immutable X.
* Else, initiate only the device auth login flow.
@@ -182,7 +189,7 @@ class IMMUTABLE_API UImmutablePassport : public UObject
* FImtblPassportResponseDelegate to call on response from JS.
*/
void ZkEvmSignTypedDataV4(const FString& RequestJsonString, const FImtblPassportResponseDelegate& ResponseDelegate);

/**
* Gets the currently saved ID token without verifying its validity.
*
@@ -286,29 +293,32 @@ class IMMUTABLE_API UImmutablePassport : public UObject
*/
static TArray<FString> GetResponseResultAsStringArray(const FImtblJSResponse& Response);

#if PLATFORM_ANDROID
#if PLATFORM_ANDROID | PLATFORM_WINDOWS
/**
* Handle deep linking. This is called from Android JNI.
* Handle deep linking. This is called from platform-specific code.
*
* @param DeepLink The deep link URL, passed from the Android JNI. This string contains the deep link data to be processed.
* @param DeepLink The deep link URL to process.
*/
void HandleDeepLink(FString DeepLink) const;

/*
* Handles the dismissal of custom tabs.
*
* @param Url The URL associated with the custom tab that was dismissed.
*/
void HandleCustomTabsDismissed(FString Url);
#elif PLATFORM_IOS | PLATFORM_MAC
#endif
#if PLATFORM_IOS | PLATFORM_MAC
/**
* Handle deep linking. This is called from iOS/Mac native code.
*
* @param DeepLink The deep link URL, passed from the iOS/Mac. This string contains the deep link data to be processed.
*/
void HandleDeepLink(NSString* sDeepLink) const;
#endif


#if PLATFORM_ANDROID
/**
* Handle the dismissal of custom tabs.
*
* @param Url The URL associated with the custom tab that was dismissed.
*/
void HandleCustomTabsDismissed(FString Url);
#endif

protected:
#if PLATFORM_ANDROID
/*
@@ -317,11 +327,14 @@ class IMMUTABLE_API UImmutablePassport : public UObject
DECLARE_DELEGATE(FImtblPassportOnPKCEDismissedDelegate);
#endif

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
/*
* Delegate used for handling deep links.
*/
DECLARE_DELEGATE_OneParam(FImtblPassportHandleDeepLinkDelegate, FString);
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
/** Delegate for handling deep link activation. */
FImmutableDeepLinkMulticastDelegate OnHandleDeepLink;
// Since the second part of PKCE is triggered by deep links, saving the
// response delegate here so it's easier to get
FImtblPassportResponseDelegate PKCEResponseDelegate;
/** Delegate for handling PCKE logout. */
FImtblPassportResponseDelegate PKCELogoutResponseDelegate;
#endif

/**
@@ -413,7 +426,7 @@ class IMMUTABLE_API UImmutablePassport : public UObject
void OnConfirmCodeResponse(FImtblJSResponse Response);

// mobile platform callbacks
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC | PLATFORM_WINDOWS
/**
* Callback from Get PKCE Auth URL.
*
@@ -428,13 +441,6 @@ class IMMUTABLE_API UImmutablePassport : public UObject
*/
void OnConnectPKCEResponse(FImtblJSResponse Response);

/*
* Callback when deep link is activated.
*
* @param DeepLink The deep link URL that was activated.
*/
void OnDeepLinkActivated(FString DeepLink);

/*
* Completes the PKCE login flow using the provided URL.
*
@@ -443,6 +449,14 @@ class IMMUTABLE_API UImmutablePassport : public UObject
void CompleteLoginPKCEFlow(FString Url);
#endif

/*
* Callback when deep link is activated.
*
* @param DeepLink The deep link URL that was activated.
*/
UFUNCTION()
void OnDeepLinkActivated(const FString& DeepLink);

#if PLATFORM_ANDROID
/**
* Callback when Login PKCE is dismissed.
@@ -501,18 +515,6 @@ class IMMUTABLE_API UImmutablePassport : public UObject
FImtblPassportOnPKCEDismissedDelegate OnPKCEDismissed;
#endif

#if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC
/** Delegate for handling deep link activation. */
FImtblPassportHandleDeepLinkDelegate OnHandleDeepLink;
// Since the second part of PKCE is triggered by deep links, saving the
// response delegate here so it's easier to get
FImtblPassportResponseDelegate PKCEResponseDelegate;
/** Delegate for handling PCKE logout. */
FImtblPassportResponseDelegate PKCELogoutResponseDelegate;
// bool IsPKCEConnected = false;
#endif


private:
/**
* Saves the current Passport settings to save game object.
@@ -547,4 +549,11 @@ class IMMUTABLE_API UImmutablePassport : public UObject
UPROPERTY()
class UImmutableAnalytics* Analytics = nullptr;

};
/**
* PKCE data used for PKCE operations e.g. login and logout flows.
* When null, no PKCE operation is currently in progress.
* When non-null, an active PKCE operation is being processed.
*/
UPROPERTY(Transient)
TObjectPtr<UImmutablePKCEData> PKCEData;
};
169 changes: 169 additions & 0 deletions Source/Immutable/Public/Immutable/Windows/ImmutablePKCEWindows.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#pragma once

#include "Kismet/BlueprintFunctionLibrary.h"

#include "Windows/WindowsHWrapper.h"

#include "Immutable/ImmutableDataTypes.h"

#include "ImmutablePKCEWindows.generated.h"

/**
* Self-contained registry watcher that creates and manages its own thread
* Monitors the registry for deep link callbacks and processes them
*/
class FRegistryWatcherRunnable : public FRunnable
{
public:
FRegistryWatcherRunnable(const FString& InRegistryKeyPath, TWeakObjectPtr<UImmutablePKCEData> InPKCEData);
virtual ~FRegistryWatcherRunnable() override;

/** FRunnable: @Interface @Begin */
virtual bool Init() override;
virtual uint32 Run() override;
virtual void Stop() override;
virtual void Exit() override;
/** FRunnable: @Interface @End */

/**
* Starts the watcher thread
* @param ProtocolName Name of the protocol for thread identification
* @return True if thread was started successfully
*/
bool StartThread(const FString& ProtocolName);

private:
FString RegistryKeyPath;
TWeakObjectPtr<UImmutablePKCEData> PKCEData;
volatile bool bShouldRun = true;

HANDLE EventHandle;
HANDLE TerminateEventHandle;
HKEY DeepLinkKeyHandle = nullptr;

// Thread that runs this runnable
TUniquePtr<FRunnableThread> Thread;
};

/**
* Windows platform-specific implementation for PKCE authentication flow
* Handles custom URI protocol registration and deep link processing
*/
UCLASS()
class IMMUTABLE_API UImmutablePKCEWindows : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

public:
/**
* Initialize the Windows deep linking functionality
*
* @param PassportInitData The initialization data containing redirect URIs and client configuration
* @return Pointer to the initialized PKCE data object or nullptr if initialization failed
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static UImmutablePKCEData* Initialise(const FImmutablePassportInitData& PassportInitData);

/**
* Register protocol handler for custom URI scheme in Windows registry
* Creates necessary registry entries and PowerShell helper scripts
*
* @param Protocol The protocol to register (e.g., "immutable")
* @return True if registration was successful, false if failed due to invalid protocol or registry errors
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static bool RegisterProtocolHandler(const FString& Protocol);

/**
* Process a deep link URL and trigger the appropriate callback
* Parses the incoming URI and extracts authentication parameters
*
* @param PKCEData The PKCE data object containing callback information; if nullptr, function will return early
* @param DeepLink The deep link URL to handle with authorization code and state
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static void HandleDeepLink(UImmutablePKCEData* PKCEData, const FString& DeepLink);

/**
* Check the registry for deep link values using a provided registry key handle
* Called by the registry watcher thread when registry changes are detected
*
* @param PKCEData The PKCE data object containing callback information
* @param DeepLinkKeyHandle An open registry key handle with appropriate permissions
* @return True if a deep link was found and processed, false if invalid parameters or no deep link found
*/
static bool CheckDeepLinkRegistry(UImmutablePKCEData* PKCEData, HKEY DeepLinkKeyHandle);

/**
* Reset and clean up PKCE operation footprint
* Stops any registry watcher threads and cleans up resources
*
* @param PKCEData The PKCE data object to clean up; if nullptr, function will return early
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static void Reset(UImmutablePKCEData* PKCEData);

protected:
/**
* Get the PowerShell script file path for a protocol
* This script handles deep link registration and browser-to-app communication
*
* @param Protocol The protocol to get the script path for
* @return The full path to the PowerShell script file in the protocol-specific directory
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static FString GetPowerShellScriptPath(const FString& Protocol);

/**
* Get the log file path for a protocol
* Used to monitor and debug deep link communication
*
* @param Protocol The protocol to get the log path for
* @return The full path to the log file in the protocol-specific directory
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static FString GetLogFilePath(const FString& Protocol);

/**
* Creates and returns a protocol-specific directory for storing PKCE-related files
* The location varies based on build configuration:
* - In shipping builds: Uses the system temp directory (%TEMP%)
* - In non-shipping builds: Uses a plugin subdirectory in the project's saved folder
*
* @param Protocol The protocol to create a directory for; if empty, returns a default path
* @return The full path to a directory dedicated to the specified protocol
*/
static FString GetProtocolDirectory(const FString& Protocol);

/**
* Get the registry key path for protocol handler registration
* Windows uses these registry entries to route custom URI schemes
*
* @param Protocol The protocol to get the registry key path for
* @return The full Windows registry key path for protocol registration
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static FString GetProtocolRegistryKeyPath(const FString& Protocol);

/**
* Get the registry key path for deep link storage
* Used to store and retrieve incoming authentication callbacks
*
* @param Protocol The protocol to get the registry key path for
* @return The registry key path where deep links are temporarily stored
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static FString GetDeepLinkRegistryKeyPath(const FString& Protocol);

/**
* Extract the protocol scheme from a URI
* Parses URIs like "immutable://callback?code=abc123&state=xyz789" to extract just the protocol name "immutable"
* The extracted protocol is used to locate the corresponding registry entries and handlers
*
* @param Uri The URI to extract the protocol from
* @param OutProtocol The output parameter to store the extracted protocol
* @return True if extraction was successful, false if URI is empty or malformed
*/
UFUNCTION(BlueprintCallable, Category = "Immutable|PKCE|Windows")
static bool ExtractProtocolFromUri(const FString& Uri, FString& OutProtocol);
};