1 Nov 2023
How to authenticate 3-legged token with your desktop applications (Win32)
With increased APIs provided by Autodesk Platform Service (APS), we may want to use APS with desktop applications. It is easy to get a 2-legged token, but 3-legged token requires user input. An easy solution could be embedding a web browser like CEF or WebView2 with your Applications. However, it is overkill if your 3-legged token is the only need and could make your application distribution very bloated.
For these kinds of simple usage, we can use the browser installed on customers’ systems. To achieve that, we need to register a protocol on the system, and let the browser start our application with code returned by APS Authentication service. Meanwhile, when we get the code from browser, we will need to check if there is an instance of our application that is already running and send the code to the process.
Here are guides for three kinds of mainstream systems. My program will register an apsshelldemo protocol, you can change to other names as you want. They are written with the basic development tools in C++/Swift. If you are using a different toolset, you could use them as a reference. In the production environment, you also need to consider how to respond to malicious data. Because custom protocols can receive data from untrusted sources, and you will need to identify and ignore them.
We are going to implement a 3-legged authentication workflow. If you are not familiar with that, our documentation provides a nice tutorial about it.
Before we are writing any code, we need to add our protocol link to our app.
Open your application settings on APS website and add a Callback URL like below:
You cannot just use apsshelldemo://, you will need to add an endpoint to it.
Windows uses registry to manage protocols and shell applications. We will need to create some registry keys under HKCR(HKEY_CLASSES_ROOT).
They are
HKCR\YourKey
Default=URL:Description
URL Protocol=YourProtocol
HKCR\YourKey\shell\open\command
Default=YourAppExecutable.exe
Here is a batch file for creating them. You will need to run it with administrator privilege.
reg add HKCR\%1 /ve /d "URL:Description"
reg add HKCR\%1 /v "URL Protocol" /d "%2"
reg add HKCR\%1\shell
reg add HKCR\%1\shell\open
reg add HKCR\%1\shell\open\command /ve /d ""%3" ""%%1""
e.g., registryProtocol.bat YourKey YourProtocol Executable
After registering our protocol, when you are trying to open yourprotocol:// in the browser, windows will pop up and ask if you want to launch your application. e.g.,
Now we are going to write our application. In this tutorial, we are going to use Win32 API. You can use them in other languages like C#, Python, Go… We will start with Windows Desktop Application template in Visual Studio. After creating your project, it will run as a blank application like below
There are several ways to check if your program is running on your system. On Windows, Mutexes can be created system wide and managed by kernel. We will create a mutex here, e.g.
// Mutex name
LPCWSTR myMutexName = L"APSAuthShell";
...
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
HANDLE mutex = CreateMutex(NULL, false, myMutexName);
if (WaitForSingleObject(mutex, 1) == WAIT_OBJECT_0)
{
// TODO: Our program isn't running before. It's our main instance.
// The original windows message loop code should be here
// Initialize global strings
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_SHELLWIN, szWindowClass, MAX_LOADSTRING);
...
return (int)msg.wParam;
}
else
{
//TODO: We already have an instance running, we want to pass our input to the instance.
return 0;
}
...
}
Our program could check if it is running now. Let’s add opening browser capability to our app next.
First, let’s prepare the authentication link first. I stored my APS credentials with my system environment variables. So, I’ll read them at the beginning of our main instance.
...
// Calling address after adding client id. We store it globally.
TCHAR authAddress[1024];
...
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
...
if (WaitForSingleObject(mutex, 1) == WAIT_OBJECT_0)
{
LPCWSTR clientIdEnvName = L"APS_CLIENT_ID";
TCHAR clientID[128];
if (!GetEnvironmentVariable(clientIdEnvName, clientID, 128 * sizeof(TCHAR)))
{
MessageBox(0, L"Please add a valid APS_CLIENT_ID in your system environment", L"Error", MB_OK);
ExitProcess(1);
return 0;
};
// We are using a string format here, %% is escaped char for %.
// If you are going to hardcode your client id in the url, it should be %20.
LPCWSTR authAddressFMT = L"https://developer.api.autodesk.com/authentication/v2/authorize?response_type=code"
L"&client_id=%s"
L"&redirect_uri=apsshelldemo://oauth"
L"&scope=data:read%%20data:create%%20data:write";
swprintf_s(authAddress, 1024, authAddressFMT, clientID);
...
}
...
}
...
Now, we want to add a menu item for us to click and open the authentication link with the system browser. Open the .rc file in your project. It is in the Resource File folder and should have the same name as your project.
Choose Menu in Resource View and select the existing menu resource. Add a menu item and give it an ID in the properties window. We’ll use the ID later. In my project, the id is ID_FILE_DOAUTH, you can change it to any ID you want.
Here is the result:
We want to add a click event for it. Windows desktop programs use windows messages to process input from user or other processes. For our menu, we just need to add a code block for processing the message.
When processing the message, we would like to open the authentication link with the system browser. To achieve that, we will need to use ShellExecute api provided by shellapi.h.
The code is quite simple.
...
#include <shellapi.h>
...
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
...
case ID_FILE_DOAUTH:
// Use shell api to open the address, windows will use the default browser.
ShellExecute(hWnd, L"open", authAddress, 0, 0, 0);
break;
...
After that, when you click at the menu item we have added earlier, it will launch system default browser and visit our authentication link.
Finally, we will need to process the callback from the browser. When the authentication calls back from browser, it will launch the application with the link as a parameter. We will pass this parameter to our existing instance and display it.
In this tutorial, we’ll use WM_COPYDATA. It requires you to find the main window of our program and send WM_COPYDATA message to it. To do that, we will need to find our process and the handle of the main window.
Let us begin with the process. We need to use APIs provided by psapi.h. We will compare the executable path current process and other processes.
...
#include <psapi.h>
...
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
...
if (WaitForSingleObject(mutex, 1) == WAIT_OBJECT_0)
{
...
}
else
{
// Get fullpath of current process for comparing against other processes.
TCHAR szCurrentProcessName[MAX_PATH] = TEXT("");
DWORD currentProcessId = GetCurrentProcessId();
HANDLE hCurrProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ,
FALSE, currentProcessId);
if (NULL != hCurrProcess)
{
HMODULE hMod;
DWORD cbNeeded;
if (EnumProcessModules(hCurrProcess, &hMod, sizeof(hMod),
&cbNeeded))
{
GetModuleFileNameEx(hCurrProcess, hMod, szCurrentProcessName,
sizeof(szCurrentProcessName) / sizeof(TCHAR));
}
}
CloseHandle(hCurrProcess);
// Find existing instance in all processes.
DWORD aProcesses[4096], cbNeeded, cProcesses;
EnumProcesses(aProcesses, cbNeeded, &cbNeeded);
cProcesses = cbNeeded / sizeof(DWORD);
for (size_t i = 0; i < cProcesses; ++i)
{
// Skip 0 and current process
if (!aProcesses[i] || aProcesses[i] == currentProcessId)
continue;
DWORD processID = aProcesses[i];
TCHAR szProcessName[MAX_PATH] = TEXT("<unknown>");
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ,
FALSE, processID);
if (NULL != hProcess)
{
HMODULE hMod;
DWORD cbNeeded;
if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
&cbNeeded))
{
GetModuleFileNameEx(hProcess, hMod, szProcessName,
sizeof(szProcessName) / sizeof(TCHAR));
}
// Let's compare the process full path.
// If you are calling to a different binary, you can create your own message or construct a user message.
// If there isn't a window, there are several differnt ways like pipe or socket...
if (wcscmp(szProcessName, szCurrentProcessName) == 0) {
// TODO: Find our window and send message
}
}
CloseHandle(hProcess);
}
return 0;
}
}
Finally, we need to find our window.
The most reliable way to do that is EnumWindows. It will iterate through all windows and get the hWnd in a callback. We need to create a struct to pass through target process id and get result from the callback.
// Define struct for enumerate windows
struct EnumData {
DWORD dwProcessId;
HWND hWnd;
};
Then, we will need to implement a callback for EnumWindows
// Application-defined callback for EnumWindows
BOOL CALLBACK EnumProc(HWND hWnd, LPARAM lParam) {
// Retrieve storage location for communication data
EnumData& ed = *(EnumData*)lParam;
DWORD dwProcessId = 0x0;
// Query process ID for hWnd
GetWindowThreadProcessId(hWnd, &dwProcessId);
// Apply filter - if you want to implement additional restrictions,
// this is the place to do so.
if (ed.dwProcessId == dwProcessId) {
// Found a window matching the process ID
ed.hWnd = hWnd;
// Report success
SetLastError(ERROR_SUCCESS);
// Stop enumeration
return FALSE;
}
// Continue enumeration
return TRUE;
}
Let’s wrap it up and use it for sending data.
...
HWND FindWindowFromProcessId(DWORD dwProcessId) {
EnumData ed = { dwProcessId };
if (!EnumWindows(EnumProc, (LPARAM)&ed) &&
(GetLastError() == ERROR_SUCCESS)) {
return ed.hWnd;
}
return NULL;
}
...
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
...
if (WaitForSingleObject(mutex, 1) == WAIT_OBJECT_0)
{
...
}
else{
// Initializing COPYDATASTRUCT for WM_COPYDATA
COPYDATASTRUCT* messageStruct =(COPYDATASTRUCT*) HeapAlloc(GetProcessHeap(), 0, sizeof(COPYDATASTRUCT));
messageStruct->dwData = 0;
messageStruct->cbData = wcslen(lpCmdLine)*2 + 2;
messageStruct->lpData = HeapAlloc(GetProcessHeap(), 0, messageStruct->cbData);
memcpy(messageStruct->lpData, lpCmdLine, messageStruct->cbData);
...
if (wcscmp(szProcessName, szCurrentProcessName) == 0) {
// Find the main window through EnumWindows
HWND targetWindow = FindWindowFromProcessId(processID);
if (NULL != targetWindow)
{
SendMessage(targetWindow, WM_COPYDATA, 0, (LPARAM)messageStruct);
}
}
...
HeapFree(GetProcessHeap(), 0, messageStruct->lpData);
HeapFree(GetProcessHeap(), 0, messageStruct);
return 0;
}
}
At last, we need to process WM_COPYDATA in the main instance.
...
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
...
break;
// Receive the authcode through an application launched with apsshelldemo launcher.
case WM_COPYDATA:
{
PCOPYDATASTRUCT data = (PCOPYDATASTRUCT)lParam;
MessageBox(NULL, (LPCWSTR)data->lpData, L"Receiving", MB_OK);
break;
}
...
Now we are done. After receiving the code, the main instance will pop up a message box like below:
We can continue the login from now on with the code. Note that the dialog is just used to help show we obtained the code.
Please check out the sample on Github.