FreeRDP
Loading...
Searching...
No Matches
aad_helper.c
1
20#include <stdlib.h>
21#include <string.h>
22#include <stdarg.h>
23
24#include <winpr/assert.h>
25#include <winpr/file.h>
26#include <winpr/handle.h>
27#include <winpr/json.h>
28#include <winpr/pipe.h>
29#include <winpr/string.h>
30#include <winpr/synch.h>
31#include <winpr/thread.h>
32#include <winpr/library.h>
33#include <winpr/path.h>
34
35#include <freerdp/build-config.h>
36#include <freerdp/utils/helpers.h>
37#include <freerdp/log.h>
38#include <freerdp/client/aad_helper.h>
39
40#define TAG CLIENT_TAG("common.aadauth")
41
42struct AadAuthHelper
43{
44 rdpClientContext* context;
45 HANDLE hCmdOutRead; /* parent's read end: helper -> FreeRDP responses/notifications */
46 HANDLE hCmdInWrite; /* parent's write end: FreeRDP -> helper requests */
47 HANDLE hProcess;
48 UINT32 nextId;
49
50 BYTE* buf;
51 size_t bufLen;
52};
53
54WINPR_ATTR_MALLOC(free, 1)
55static char* aad_auth_helper_detect_helper(void);
56
57/* ---- wire format helpers ------------------------------------------------------------- */
58WINPR_ATTR_MALLOC(free, 1)
59static char* build_hello_request(UINT32 id)
60{
61 WINPR_JSON* obj = WINPR_JSON_CreateObject();
62 if (!obj)
63 return nullptr;
64
65 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
66 WINPR_JSON_AddIntegerToObject(obj, "id", id) &&
67 WINPR_JSON_AddStringToObject(obj, "method", "hello");
68
69 WINPR_JSON* params = ok ? WINPR_JSON_AddObjectToObject(obj, "params") : nullptr;
70 ok = ok && params && WINPR_JSON_AddIntegerToObject(params, "protocol_version", 1) &&
71 WINPR_JSON_AddStringToObject(params, "client", "freerdp");
72
73 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
75 return str;
76}
77
78WINPR_ATTR_MALLOC(free, 1)
79static char* build_navigate_request(UINT32 id, const char* title, const char* url,
80 const char* redirect_uri, UINT32 timeout_ms)
81{
82 WINPR_JSON* obj = WINPR_JSON_CreateObject();
83 if (!obj)
84 return nullptr;
85
86 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
87 WINPR_JSON_AddIntegerToObject(obj, "id", id) &&
88 WINPR_JSON_AddStringToObject(obj, "method", "navigate");
89
90 WINPR_JSON* params = ok ? WINPR_JSON_AddObjectToObject(obj, "params") : nullptr;
91 ok = ok && params && WINPR_JSON_AddStringToObject(params, "title", title) &&
92 WINPR_JSON_AddStringToObject(params, "url", url) &&
93 WINPR_JSON_AddStringToObject(params, "redirect_uri", redirect_uri) &&
94 WINPR_JSON_AddIntegerToObject(params, "timeout_ms", timeout_ms);
95
96 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
98 return str;
99}
100
101WINPR_ATTR_MALLOC(free, 1)
102static char* build_shutdown_request(UINT32 id)
103{
104 WINPR_JSON* obj = WINPR_JSON_CreateObject();
105 if (!obj)
106 return nullptr;
107
108 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
109 WINPR_JSON_AddIntegerToObject(obj, "id", id) &&
110 WINPR_JSON_AddStringToObject(obj, "method", "shutdown");
111
112 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
114 return str;
115}
116
117WINPR_ATTR_MALLOC(free, 1)
118static char* build_exit_notification(void)
119{
120 WINPR_JSON* obj = WINPR_JSON_CreateObject();
121 if (!obj)
122 return nullptr;
123
124 BOOL ok = WINPR_JSON_AddStringToObject(obj, "jsonrpc", "2.0") &&
125 WINPR_JSON_AddStringToObject(obj, "method", "exit");
126
127 char* str = ok ? WINPR_JSON_PrintUnformatted(obj) : nullptr;
129 return str;
130}
131
132/* ---- transport: newline-delimited JSON over the helper's cmdIn/cmdOut pipes ----------- */
133WINPR_ATTR_NODISCARD
134static BOOL helper_write_line(AadAuthHelper* helper, const char* json)
135{
136 WINPR_ASSERT(helper);
137 WINPR_ASSERT(json);
138
139 const size_t len = strlen(json);
140 const size_t total = len + 1; /* trailing '\n' */
141 char* line = malloc(total);
142 if (!line)
143 return FALSE;
144 /* terminated with '\n', not '\0' - written as-is over the wire, never treated as a C string */
145 // NOLINTNEXTLINE(bugprone-not-null-terminated-result)
146 memcpy(line, json, len);
147 line[len] = '\n';
148
149 BOOL rc = TRUE;
150 size_t written = 0;
151 while (written < total)
152 {
153 DWORD dwWritten = 0;
154 if (!WriteFile(helper->hCmdInWrite, line + written, (DWORD)(total - written), &dwWritten,
155 nullptr) ||
156 (dwWritten == 0))
157 {
158 WLog_ERR(TAG, "aad-auth-helper: failed writing to helper");
159 rc = FALSE;
160 break;
161 }
162 written += dwWritten;
163 }
164
165 free(line);
166 return rc;
167}
168
170WINPR_ATTR_MALLOC(free, 1)
171static char* linebuf_extract(AadAuthHelper* helper)
172{
173 if (!helper->buf || !helper->bufLen)
174 return nullptr;
175
176 const BYTE* nl = memchr(helper->buf, '\n', helper->bufLen);
177 if (!nl)
178 return nullptr;
179
180 const size_t lineLen = (size_t)(nl - helper->buf);
181 char* line = malloc(lineLen + 1);
182 if (!line)
183 return nullptr;
184 memcpy(line, helper->buf, lineLen);
185 line[lineLen] = '\0';
186
187 const size_t consumed = lineLen + 1;
188 const size_t remaining = helper->bufLen - consumed;
189 memmove(helper->buf, helper->buf + consumed, remaining);
190 helper->bufLen = remaining;
191 return line;
192}
193
194WINPR_ATTR_MALLOC(free, 1)
195static char* helper_read_line(AadAuthHelper* helper)
196{
197 WINPR_ASSERT(helper);
198
199 char* line = linebuf_extract(helper);
200 if (line)
201 return line;
202
203 while (TRUE)
204 {
205 BYTE chunk[4096];
206 DWORD dwRead = 0;
207 if (!ReadFile(helper->hCmdOutRead, chunk, sizeof(chunk), &dwRead, nullptr) || (dwRead == 0))
208 {
209 WLog_ERR(TAG, "aad-auth-helper: helper pipe closed or read error");
210 return nullptr;
211 }
212
213 BYTE* nbuf = realloc(helper->buf, helper->bufLen + dwRead);
214 if (!nbuf)
215 return nullptr;
216 helper->buf = nbuf;
217 memcpy(helper->buf + helper->bufLen, chunk, dwRead);
218 helper->bufLen += dwRead;
219
220 line = linebuf_extract(helper);
221 if (line)
222 return line;
223 }
224}
225
229WINPR_ATTR_MALLOC(WINPR_JSON_Delete, 1)
230static WINPR_JSON* wait_for_response(AadAuthHelper* helper, UINT32 expectedId)
231{
232 while (TRUE)
233 {
234 char* line = helper_read_line(helper);
235 if (!line)
236 return nullptr;
237
238 WINPR_JSON* msg = WINPR_JSON_Parse(line);
239 free(line);
240 if (!msg)
241 {
242 WLog_WARN(TAG, "aad-auth-helper: ignoring malformed line from helper");
243 continue;
244 }
245
246 WINPR_JSON* method = WINPR_JSON_GetObjectItemCaseSensitive(msg, "method");
247 if (method && WINPR_JSON_IsString(method))
248 {
249 const char* m = WINPR_JSON_GetStringValue(method);
250 if (m && (strcmp(m, "log") == 0))
251 {
252 WINPR_JSON* params = WINPR_JSON_GetObjectItemCaseSensitive(msg, "params");
253 WINPR_JSON* message =
254 params ? WINPR_JSON_GetObjectItemCaseSensitive(params, "message") : nullptr;
255 const char* text = (message && WINPR_JSON_IsString(message))
257 : "";
258 WLog_INFO(TAG, "[helper] %s", text);
259 }
261 continue;
262 }
263
264 WINPR_JSON* id = WINPR_JSON_GetObjectItemCaseSensitive(msg, "id");
265 const double idValue = (id && WINPR_JSON_IsNumber(id)) ? WINPR_JSON_GetNumberValue(id) : 0;
266 if (!id || !WINPR_JSON_IsNumber(id) || ((UINT32)idValue != expectedId))
267 {
268 WLog_WARN(TAG, "aad-auth-helper: dropping response with unexpected id");
270 continue;
271 }
272
273 return msg;
274 }
275}
276
277static void updateBoolFromConfig(WINPR_JSON* obj, const char* what, BOOL* pVal)
278{
279 WINPR_ASSERT(obj);
280 WINPR_ASSERT(what);
281 WINPR_ASSERT(pVal);
282 WINPR_JSON* val = WINPR_JSON_GetObjectItemCaseSensitive(obj, what);
283 if (!val)
284 return;
285 if (!WINPR_JSON_IsBool(val))
286 return;
287 *pVal = WINPR_JSON_IsTrue(val);
288}
289
290static void updateStringFromConfig(WINPR_JSON* obj, const char* what, char** pVal)
291{
292 WINPR_ASSERT(obj);
293 WINPR_ASSERT(what);
294 WINPR_ASSERT(pVal);
295 WINPR_JSON* val = WINPR_JSON_GetObjectItemCaseSensitive(obj, what);
296 if (!val)
297 return;
298 if (!WINPR_JSON_IsString(val))
299 return;
300 free(*pVal);
301 *pVal = _strdup(WINPR_JSON_GetStringValue(val));
302}
303
304WINPR_ATTR_MALLOC(free, 1)
305static char* getHelperBinary(const rdpClientContext* context)
306{
307 /* TODO: Detection of helper binary:
308 *
309 * 1. TODO system wide config file? (config value/deny user level/deny command line/deny
310 * auto-detect)
311 * 2. TODO user level config file? (config value/deny command line/deny auto-detect)
312 * 3. command line parameter
313 * 4. auto detection (default)
314 */
315 char* exe = nullptr;
316
317 BOOL useArg = TRUE;
318 BOOL useDetect = TRUE;
319 BOOL useUserConfig = TRUE;
320
321 const char config[] = "freerdp-client-aad.json";
322 WINPR_JSON* sys = freerdp_GetJSONConfigFile(TRUE, config);
323 if (sys)
324 {
325 updateBoolFromConfig(sys, "allow-commandline", &useArg);
326 updateBoolFromConfig(sys, "allow-autodetect", &useDetect);
327 updateBoolFromConfig(sys, "allow-user-config", &useUserConfig);
328 updateStringFromConfig(sys, "helper-binary", &exe);
330 }
331 if (useUserConfig)
332 {
333 WINPR_JSON* user = freerdp_GetJSONConfigFile(FALSE, config);
334 if (user)
335 {
336 updateBoolFromConfig(user, "allow-commandline", &useArg);
337 updateBoolFromConfig(user, "allow-autodetect", &useDetect);
338 updateStringFromConfig(user, "helper-binary", &exe);
339 }
340 WINPR_JSON_Delete(user);
341 }
342
343 if (!exe && useArg)
344 {
345 const char* args =
346 freerdp_settings_get_string(context->context.settings, FreeRDP_AadAuthHelper);
347 if (args && (strcmp("autodetect", args) == 0))
348 exe = aad_auth_helper_detect_helper();
349 else if (args)
350 exe = _strdup(args);
351 }
352
353 if (!exe && useDetect)
354 exe = aad_auth_helper_detect_helper();
355
356 if (!exe)
357 {
358 WLog_ERR(TAG, "aad-auth-helper: no helper application detected, aborting");
359 return nullptr;
360 }
361 return exe;
362}
363
364/* ---- public API ------------------------------------------------------------------------ */
365
366AadAuthHelper* aad_auth_helper_start(rdpClientContext* context)
367{
368 WINPR_ASSERT(context);
369
370 char* exe = nullptr;
371 AadAuthHelper* helper = calloc(1, sizeof(AadAuthHelper));
372 if (!helper)
373 return nullptr;
374 helper->context = context;
375
376 PROCESS_INFORMATION procInfo = WINPR_C_ARRAY_INIT;
377 LPPROC_THREAD_ATTRIBUTE_LIST attrList = nullptr;
378 HANDLE hCmdInRead = nullptr; /* child's end, handed away via --cmdInFd= */
379 HANDLE hCmdOutWrite = nullptr; /* child's end, handed away via --cmdOutFd= */
380 char* cmdline = nullptr;
381 BOOL created = FALSE;
382
383 SECURITY_ATTRIBUTES saAttr = { .nLength = sizeof(SECURITY_ATTRIBUTES),
384 .bInheritHandle = TRUE,
385 .lpSecurityDescriptor = nullptr };
386
387 STARTUPINFOEXA siStartInfoEx = {
388 .StartupInfo.cb = sizeof(siStartInfoEx),
389 /* the JSON-RPC channel travels over two dedicated pipes handed to the helper via
390 * --cmdInFd=/--cmdOutFd= command line arguments (see winpr_exportHandleToString() below),
391 * not stdin/stdout - so the helper's stdio is left as a plain passthrough of this process'
392 * own, the same way hStdError already was. This keeps the protocol immune to anything the
393 * helper (or a library it links, e.g. Chromium/Qt) happens to print to stdout/stderr for
394 * its own diagnostics, and lets that output reach the user's terminal normally. */
395 .StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE),
396 .StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE),
397 .StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE),
398 .StartupInfo.dwFlags = STARTF_USESTDHANDLES
399 };
400
401 if (!CreatePipe(&helper->hCmdOutRead, &hCmdOutWrite, &saAttr, 0))
402 {
403 WLog_ERR(TAG, "aad-auth-helper: cmdOut CreatePipe failed");
404 goto cleanup;
405 }
406 if (!SetHandleInformation(helper->hCmdOutRead, HANDLE_FLAG_INHERIT, 0))
407 {
408 WLog_ERR(TAG, "aad-auth-helper: cmdOut SetHandleInformation failed");
409 goto cleanup;
410 }
411
412 if (!CreatePipe(&hCmdInRead, &helper->hCmdInWrite, &saAttr, 0))
413 {
414 WLog_ERR(TAG, "aad-auth-helper: cmdIn CreatePipe failed");
415 goto cleanup;
416 }
417 if (!SetHandleInformation(helper->hCmdInWrite, HANDLE_FLAG_INHERIT, 0))
418 {
419 WLog_ERR(TAG, "aad-auth-helper: cmdIn SetHandleInformation failed");
420 goto cleanup;
421 }
422
423 char cmdInArg[64] = WINPR_C_ARRAY_INIT;
424 char cmdOutArg[64] = WINPR_C_ARRAY_INIT;
425 if (!winpr_exportHandleToString(hCmdInRead, "--cmdInFd={}", cmdInArg, sizeof(cmdInArg)))
426 {
427 WLog_ERR(TAG, "aad-auth-helper: failed to export the cmdIn handle");
428 goto cleanup;
429 }
430 if (!winpr_exportHandleToString(hCmdOutWrite, "--cmdOutFd={}", cmdOutArg, sizeof(cmdOutArg)))
431 {
432 WLog_ERR(TAG, "aad-auth-helper: failed to export the cmdOut handle");
433 goto cleanup;
434 }
435
436 /* explicit allowlist: only these handles are inherited by the spawned helper, regardless of
437 * anything else in this process that happens to also be marked inheritable (e.g. by another
438 * component linked into the same client). See winpr's CreateProcess /
439 * PROC_THREAD_ATTRIBUTE_HANDLE_LIST support - without this, WinPR's CreateProcess already
440 * defaults to closing everything not wired via STARTUPINFO, so this list exists to make that
441 * contract explicit and portable to real Windows builds of this same file.
442 *
443 * UpdateProcThreadAttribute() only stores a pointer to `handles`, it does not copy it (matches
444 * real Windows - see winpr's own DeleteProcThreadAttributeList() comment) - so `handles` must
445 * stay alive until the CreateProcessA() call below returns. Deliberately kept in the same
446 * block as that call rather than a narrower nested scope: a variable can be read as
447 * use-after-scope by AddressSanitizer once its enclosing block ends even while its storage is
448 * technically still on the stack, and even though CreateProcessA() only reads it through this
449 * still-live block, an earlier version of this function that closed `handles`' scope before
450 * calling CreateProcessA() (relying only on `attrList`/`siStartInfoEx` still being valid)
451 * tripped exactly that. */
452 HANDLE handles[5] = { siStartInfoEx.StartupInfo.hStdOutput, siStartInfoEx.StartupInfo.hStdInput,
453 siStartInfoEx.StartupInfo.hStdError, hCmdInRead, hCmdOutWrite };
454 {
455 SIZE_T size = 0;
456
457 if (InitializeProcThreadAttributeList(nullptr, 1, 0, &size) || (size == 0))
458 {
459 WLog_ERR(TAG, "aad-auth-helper: unexpected attribute list sizing result");
460 goto cleanup;
461 }
462
463 attrList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size);
464 if (!attrList || !InitializeProcThreadAttributeList(attrList, 1, 0, &size))
465 {
466 WLog_ERR(TAG, "aad-auth-helper: InitializeProcThreadAttributeList failed");
467 goto cleanup;
468 }
469
470 if (!UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
471 (PVOID)handles, sizeof(handles), nullptr, nullptr))
472 {
473 WLog_ERR(TAG, "aad-auth-helper: UpdateProcThreadAttribute failed");
474 goto cleanup;
475 }
476
477 siStartInfoEx.lpAttributeList = attrList;
478 }
479
480 {
481 size_t cmdlineLen = 0;
482 exe = getHelperBinary(context);
483 if (!exe)
484 goto cleanup;
485 const int rc =
486 winpr_asprintf(&cmdline, &cmdlineLen, "\"%s\" %s %s", exe, cmdInArg, cmdOutArg);
487 if (rc < 0)
488 goto cleanup;
489
490 created =
491 CreateProcessA(nullptr, cmdline, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT,
492 nullptr, nullptr, (LPSTARTUPINFOA)&siStartInfoEx, &procInfo);
493 }
494
495 if (!created)
496 WLog_ERR(TAG, "aad-auth-helper: failed to spawn '%s'", exe);
497
498cleanup:
499 free(exe);
500 free(cmdline);
501 if (attrList)
502 {
503 DeleteProcThreadAttributeList(attrList);
504 free(attrList);
505 }
506 (void)CloseHandle(procInfo.hThread);
507 if (hCmdInRead)
508 (void)CloseHandle(hCmdInRead);
509 if (hCmdOutWrite)
510 (void)CloseHandle(hCmdOutWrite);
511
512 if (!created)
513 {
514 aad_auth_helper_stop(helper);
515 return nullptr;
516 }
517
518 helper->hProcess = procInfo.hProcess;
519
520 {
521 const UINT32 id = ++helper->nextId;
522 char* req = build_hello_request(id);
523 BOOL ok = req && helper_write_line(helper, req);
524 free(req);
525
526 if (ok)
527 {
528 WINPR_JSON* resp = wait_for_response(helper, id);
529 ok = resp && WINPR_JSON_HasObjectItem(resp, "result");
530 if (resp)
531 WINPR_JSON_Delete(resp);
532 }
533
534 if (!ok)
535 {
536 WLog_ERR(TAG, "aad-auth-helper: hello handshake failed");
537 aad_auth_helper_stop(helper);
538 return nullptr;
539 }
540 }
541
542 return helper;
543}
544
545static AadAuthHelperNavigateStatus aad_auth_helper_navigate(AadAuthHelper* helper,
546 const char* title, const char* url,
547 const char* redirect_uri,
548 UINT32 timeout_ms, char** redirect_url,
549 size_t* redirect_url_len)
550{
551 WINPR_ASSERT(helper);
552 WINPR_ASSERT(url);
553 WINPR_ASSERT(redirect_uri);
554 WINPR_ASSERT(redirect_url);
555 WINPR_ASSERT(redirect_url_len);
556
557 *redirect_url = nullptr;
558 *redirect_url_len = 0;
559
560 const UINT32 id = ++helper->nextId;
561 char* req = build_navigate_request(id, title ? title : "", url, redirect_uri, timeout_ms);
562 if (!req)
563 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
564
565 BOOL ok = helper_write_line(helper, req);
566 free(req);
567 if (!ok)
568 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
569
570 WINPR_JSON* resp = wait_for_response(helper, id);
571 if (!resp)
572 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
573
574 WINPR_JSON* error = WINPR_JSON_GetObjectItemCaseSensitive(resp, "error");
575 if (error)
576 {
577 WINPR_JSON* message = WINPR_JSON_GetObjectItemCaseSensitive(error, "message");
578 const char* msg = (message && WINPR_JSON_IsString(message))
580 : "unknown error";
581 WLog_WARN(TAG, "aad-auth-helper: navigate failed: %s", msg);
582
583 AadAuthHelperNavigateStatus status = AAD_AUTH_HELPER_NAVIGATE_ERROR;
584 if (strcmp(msg, "user_cancelled") == 0)
585 status = AAD_AUTH_HELPER_NAVIGATE_CANCELLED;
586 else if (strcmp(msg, "timeout") == 0)
587 status = AAD_AUTH_HELPER_NAVIGATE_TIMEOUT;
588
589 WINPR_JSON_Delete(resp);
590 return status;
591 }
592
593 WINPR_JSON* result = WINPR_JSON_GetObjectItemCaseSensitive(resp, "result");
594 WINPR_JSON* urlItem =
595 result ? WINPR_JSON_GetObjectItemCaseSensitive(result, "redirect_url") : nullptr;
596 const char* value =
597 (urlItem && WINPR_JSON_IsString(urlItem)) ? WINPR_JSON_GetStringValue(urlItem) : nullptr;
598
599 if (!value)
600 {
601 WLog_ERR(TAG, "aad-auth-helper: malformed navigate result");
602 WINPR_JSON_Delete(resp);
603 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
604 }
605
606 *redirect_url = _strdup(value);
607 if (*redirect_url)
608 *redirect_url_len = strlen(*redirect_url);
609 WINPR_JSON_Delete(resp);
610 return (*redirect_url != nullptr) ? AAD_AUTH_HELPER_NAVIGATE_OK
611 : AAD_AUTH_HELPER_NAVIGATE_ERROR;
612}
613
614void aad_auth_helper_stop(AadAuthHelper* helper)
615{
616 if (!helper)
617 return;
618
619 if (helper->hProcess)
620 {
621 const UINT32 id = ++helper->nextId;
622 char* req = build_shutdown_request(id);
623 if (req && helper_write_line(helper, req))
624 {
625 WINPR_JSON* resp = wait_for_response(helper, id);
626 if (resp)
627 WINPR_JSON_Delete(resp);
628 }
629 free(req);
630
631 char* notif = build_exit_notification();
632 if (notif)
633 (void)helper_write_line(helper, notif);
634 free(notif);
635
636 if (WaitForSingleObject(helper->hProcess, 3000) != WAIT_OBJECT_0)
637 {
638 WLog_WARN(TAG, "aad-auth-helper: did not exit in time, terminating");
639 (void)TerminateProcess(helper->hProcess, 0);
640 }
641 (void)CloseHandle(helper->hProcess);
642 }
643
644 if (helper->hCmdInWrite)
645 (void)CloseHandle(helper->hCmdInWrite);
646 if (helper->hCmdOutRead)
647 (void)CloseHandle(helper->hCmdOutRead);
648
649 free(helper->buf);
650 free(helper);
651}
652
653WINPR_ATTR_MALLOC(winpr_zfree, 1)
654static char* aad_auth_helper_extract_query_param(const char* url, const char* name)
655{
656 if (!url || !name)
657 return nullptr;
658
659 const char* start = strchr(url, '?');
660 if (!start)
661 return nullptr;
662
663 const char* param = strstr(start, name);
664 if (!param)
665 return nullptr;
666
667 const size_t len = strlen(name);
668 if (param[len] != '=')
669 return nullptr;
670
671 char* str = _strdup(&param[len + 1]);
672 if (!str)
673 return nullptr;
674
675 char* end = strchr(str, '&');
676 if (end)
677 *end = '\0';
678 const size_t slen = strlen(str);
679 char* decoded = winpr_str_url_decode(str, slen);
680 winpr_zfree(str);
681 return decoded;
682}
683
691WINPR_ATTR_NODISCARD
692static AadAuthHelperNavigateStatus aad_helper_navigate(AadAuthHelper* helper, const char* title,
693 const char* url, char** pRedirectUrl,
694 size_t* pRedirectUrlLen)
695{
696 WINPR_ASSERT(helper);
697 WINPR_ASSERT(title);
698 WINPR_ASSERT(url);
699 WINPR_ASSERT(pRedirectUrl);
700 WINPR_ASSERT(pRedirectUrlLen);
701
702 *pRedirectUrl = nullptr;
703 *pRedirectUrlLen = 0;
704
705 char* redirectUri = aad_auth_helper_extract_query_param(url, "redirect_uri");
706 if (!redirectUri)
707 {
708 WLog_ERR(TAG, "[aad-auth] url %s has no redirect_uri parameter", url);
709 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
710 }
711
712 char* out = nullptr;
713 size_t outLen = 0;
714 const AadAuthHelperNavigateStatus status =
715 aad_auth_helper_navigate(helper, title, url, redirectUri, 180000, &out, &outLen);
716 winpr_zfree(redirectUri);
717 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
718 {
719 free(out);
720 return status;
721 }
722
723 *pRedirectUrl = out;
724 *pRedirectUrlLen = outLen;
725 return AAD_AUTH_HELPER_NAVIGATE_OK;
726}
727
728WINPR_ATTR_NODISCARD
729static BOOL aad_auth_helper_get_rdsaad_access_token(AadAuthHelper* helper,
730 freerdp_client_aad_type requestType,
731 freerdp_client_aad_type tokenType,
732 const char* scope, const char* req_cnf,
733 char** token)
734{
735 WINPR_ASSERT(helper);
736 WINPR_ASSERT(scope);
737 WINPR_ASSERT(req_cnf);
738 WINPR_ASSERT(token);
739
740 rdpClientContext* cctx = helper->context;
741 WINPR_ASSERT(cctx);
742
743 const char* title = "FreeRDP WebView - AAD access token";
744 if (requestType == FREERDP_CLIENT_AAD_AVD_AUTH_REQUEST)
745 title = "FreeRDP WebView - AVD access token";
746
747 char* request = freerdp_client_get_aad_url(cctx, requestType, scope);
748 if (!request)
749 {
750 WLog_ERR(TAG, "[aad-auth] authentication failed, could not construct request");
751 return FALSE;
752 }
753
754 char* redirectUrl = nullptr;
755 size_t redirectUrlLen = 0;
756 const AadAuthHelperNavigateStatus status =
757 aad_helper_navigate(helper, title, request, &redirectUrl, &redirectUrlLen);
758 winpr_zfree(request);
759
760 if (status == AAD_AUTH_HELPER_NAVIGATE_CANCELLED)
761 {
762 winpr_znfree(redirectUrl, redirectUrlLen);
763 WLog_INFO(TAG, "[aad-auth] user cancelled the authentication");
764 return FALSE;
765 }
766 if (status == AAD_AUTH_HELPER_NAVIGATE_TIMEOUT)
767 {
768 winpr_znfree(redirectUrl, redirectUrlLen);
769 WLog_ERR(TAG, "[aad-auth] authentication timed out");
770 return FALSE;
771 }
772 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
773 {
774 winpr_znfree(redirectUrl, redirectUrlLen);
775 WLog_ERR(TAG, "[aad-auth] authentication failed");
776 return FALSE;
777 }
778
779 char* code = freerdp_client_extract_aad_code(cctx, redirectUrl, redirectUrlLen);
780 winpr_znfree(redirectUrl, redirectUrlLen);
781
782 if (!code)
783 {
784 WLog_ERR(TAG, "[aad-auth] authentication failed, could not find code parameter");
785 return FALSE;
786 }
787
788 char* token_request = nullptr;
789 if (tokenType == FREERDP_CLIENT_AAD_TOKEN_REQUEST)
790 token_request = freerdp_client_get_aad_url(cctx, tokenType, scope, code, req_cnf);
791 else
792 token_request = freerdp_client_get_aad_url(cctx, tokenType, code);
793 winpr_zfree(code);
794 if (!token_request)
795 {
796 WLog_ERR(TAG, "[aad-auth] authentication failed, could not get token");
797 return FALSE;
798 }
799
800 const BOOL rc = client_common_get_access_token(cctx->context.instance, token_request, token);
801 winpr_zfree(token_request);
802 return rc;
803}
804
805BOOL aad_auth_helper_get_access_token_v(AadAuthHelper* helper, AccessTokenType tokenType,
806 char** token, size_t count, va_list args)
807{
808 WINPR_ASSERT(token);
809 switch (tokenType)
810 {
811 case ACCESS_TOKEN_TYPE_AAD:
812 {
813 if (count < 2)
814 {
815 WLog_ERR(TAG,
816 "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
817 ", aborting",
818 count);
819 return FALSE;
820 }
821 else if (count > 2)
822 WLog_WARN(TAG,
823 "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
824 ", ignoring",
825 count);
826 const char* scope = va_arg(args, const char*);
827 const char* req_cnf = va_arg(args, const char*);
828 return aad_auth_helper_get_rdsaad_access_token(helper, FREERDP_CLIENT_AAD_AUTH_REQUEST,
829 FREERDP_CLIENT_AAD_TOKEN_REQUEST, scope,
830 req_cnf, token);
831 }
832 case ACCESS_TOKEN_TYPE_AVD:
833 if (count != 0)
834 WLog_WARN(TAG,
835 "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz
836 ", ignoring",
837 count);
838 return aad_auth_helper_get_rdsaad_access_token(
839 helper, FREERDP_CLIENT_AAD_AVD_AUTH_REQUEST, FREERDP_CLIENT_AAD_AVD_TOKEN_REQUEST,
840 "", "", token);
841 default:
842 WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIu32 "], aborting",
843 tokenType);
844 return FALSE;
845 }
846}
847
848BOOL aad_auth_helper_get_access_token(AadAuthHelper* helper, AccessTokenType tokenType,
849 char** token, size_t count, ...)
850{
851 va_list ap = WINPR_C_ARRAY_INIT;
852 va_start(ap, count);
853 const BOOL rc = aad_auth_helper_get_access_token_v(helper, tokenType, token, count, ap);
854 va_end(ap);
855 return rc;
856}
857
858/* whether any auto-detectable helper was enabled at build time at all - see
859 * WITH_XDG_AAD_AUTH_HELPER / WITH_WEBVIEW_AAD_AUTH_HELPER / WITH_QT_AAD_AUTH_HELPER in
860 * client/common/CMakeLists.txt, propagated here as compile definitions by
861 * client/SDL/common/CMakeLists.txt. Guards kHelperCandidates below: with none of the three
862 * defined there's nothing to list, and a zero-size array isn't valid standard C++. */
863
864/* auto-pick order for /azure:auth-helper:autodetect (or the option omitted entirely): xdg-open
865 * first (drives the user's actual default browser, so it inherits whatever SSO session/cookies
866 * are already there instead of prompting again), then the embedded webview (lighter, native OS
867 * look), then Qt. */
868static const char* kHelperCandidates[] = { "freerdp-xdg-aad-helper", "freerdp-qt-aad-helper",
869 "freerdp-webview-aad-helper" };
870
871static void helper_binary_dirs_free(char** dirs, size_t count)
872{
873 if (!dirs)
874 return;
875 for (size_t x = 0; x < count; x++)
876 {
877 char* dir = dirs[x];
878 free(dir);
879 }
880 free((void*)dirs);
881}
882
883WINPR_ATTR_MALLOC(free, 1)
884static char* aad_auth_helper_get_binary_dir(void)
885{
886 DWORD len = 4096;
887 char* path = nullptr;
888 do
889 {
890 char* tmp = realloc(path, len);
891 if (!tmp)
892 {
893 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
894 free(path);
895 return nullptr;
896 }
897 path = tmp;
898
899 const DWORD rc = GetModuleFileNameA(nullptr, path, len);
900 if (rc == 0)
901 {
902 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
903 free(path);
904 return nullptr;
905 }
906
907 if (rc == len)
908 {
909 if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
910 {
911 len += 4096;
912 continue;
913 }
914 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
915 free(path);
916 return nullptr;
917 }
918 else
919 break;
920 } while (TRUE);
921
922 char* sep = strrchr(path, '/');
923#ifdef _WIN32
924 char* sepWin = strrchr(path, '\\');
925 if (!sep || (sepWin && (sepWin > sep)))
926 sep = sepWin;
927#endif
928 if (!sep)
929 {
930 free(path);
931 return nullptr;
932 }
933 *sep = '\0';
934 return path;
935}
936
937/* directory this client binary itself lives in - where an installed (or freshly built) helper
938 * binary is expected to sit alongside it. */
939WINPR_ATTR_MALLOC(helper_binary_dirs_free, 1)
940static char** aad_auth_helper_binary_dirs(size_t* count)
941{
942 WINPR_ASSERT(count);
943
944 *count = 0;
945 char** dirs = (char**)calloc(32, sizeof(char*));
946 if (!dirs)
947 return nullptr;
948 {
949 char* libexec = GetCombinedPath(FREERDP_INSTALL_PREFIX, FREERDP_LIBEXEC_PATH);
950 if (libexec)
951 dirs[(*count)++] = libexec;
952 }
953
954 char* app = aad_auth_helper_get_binary_dir();
955 if (app)
956 {
957 dirs[(*count)++] = app;
958
959 char* libexec = GetCombinedPath(app, FREERDP_LIBEXEC_REL_PATH);
960 if (libexec)
961 dirs[(*count)++] = libexec;
962 }
963 return dirs;
964}
965
966WINPR_ATTR_MALLOC(free, 1)
967static char* aad_auth_helper_path_for_binary(const char* dir, const char* binaryName)
968{
969 const char extension[] = CMAKE_EXECUTABLE_SUFFIX;
970
971 char* path = nullptr;
972 size_t plen = 0;
973 winpr_asprintf(&path, &plen, "%s/%s%s", dir, binaryName, extension);
974 return path;
975}
976
977/* /azure:auth-helper:autodetect (or the option omitted entirely): probe the well-known binaries
978 * in kHelperCandidates order and use whichever is actually present. */
979WINPR_ATTR_MALLOC(free, 1)
980static char* aad_auth_helper_auto_locate(void)
981{
982 size_t dirscount = 0;
983 char** dirs = aad_auth_helper_binary_dirs(&dirscount);
984 if (!dirs)
985 return nullptr;
986
987 char* path = nullptr;
988 for (size_t i = 0; i < dirscount; i++)
989 {
990 char* dir = dirs[i];
991 if (!dir)
992 continue;
993 for (size_t x = 0; x < ARRAYSIZE(kHelperCandidates); x++)
994 {
995 const char* binaryName = kHelperCandidates[x];
996 char* cpath = aad_auth_helper_path_for_binary(dir, binaryName);
997 if (winpr_PathFileExists(cpath))
998 {
999 path = cpath;
1000 break;
1001 }
1002 free(cpath);
1003 }
1004 if (path)
1005 break;
1006 }
1007
1008 helper_binary_dirs_free(dirs, dirscount);
1009 return path;
1010}
1011
1012/* @p helper is the caller's own per-connection storage slot (e.g. a member of its SdlContext) -
1013 * this file never stores anything itself, so it stays usable as one binary shared between the
1014 * SDL2 and SDL3 clients regardless of their (different) concrete SdlContext type. */
1015char* aad_auth_helper_detect_helper(void)
1016{
1017 char* path = aad_auth_helper_auto_locate();
1018
1019 if (!path)
1020 {
1021 WLog_ERR(TAG, "[aad-auth] could not determine expected helper binary location");
1022 return nullptr;
1023 }
1024
1025 if (!winpr_PathFileExists(path))
1026 {
1027 WLog_ERR(TAG, "[aad-auth] helper binary not found at '%s'", path);
1028 free(path);
1029 return nullptr;
1030 }
1031
1032 WLog_DBG(TAG, "[aad-auth] auto-detected helper %s", path);
1033 return path;
1034}
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_AddObjectToObject(WINPR_JSON *object, const char *name)
WINPR_JSON_AddObjectToObject.
Definition c-json.c:274
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_CreateObject(void)
WINPR_JSON_CreateObject.
Definition c-json.c:232
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_HasObjectItem(const WINPR_JSON *object, const char *string)
Check if JSON has an object matching the name.
Definition c-json.c:132
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsBool(const WINPR_JSON *item)
Check if JSON item is of type BOOL.
Definition c-json.c:167
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsNumber(const WINPR_JSON *item)
Check if JSON item is of type Number.
Definition c-json.c:177
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_AddIntegerToObject(WINPR_JSON *object, const char *name, int64_t number)
WINPR_JSON_AddIntegerToObject.
Definition c-json.c:262
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsTrue(const WINPR_JSON *item)
Check if JSON item is BOOL value True.
Definition c-json.c:162
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_GetObjectItemCaseSensitive(const WINPR_JSON *object, const char *string)
Same as WINPR_JSON_GetObjectItem but with case sensitive matching.
Definition c-json.c:127
WINPR_ATTR_NODISCARD WINPR_API BOOL WINPR_JSON_IsString(const WINPR_JSON *item)
Check if JSON item is of type String.
Definition c-json.c:182
WINPR_API char * WINPR_JSON_PrintUnformatted(WINPR_JSON *item)
Serialize a JSON instance to string without formatting for human readable formatted output see WINPR_...
Definition c-json.c:311
WINPR_ATTR_NODISCARD WINPR_API double WINPR_JSON_GetNumberValue(const WINPR_JSON *item)
Return the Number value of a JSON item.
Definition c-json.c:147
WINPR_ATTR_NODISCARD WINPR_API WINPR_JSON * WINPR_JSON_AddStringToObject(WINPR_JSON *object, const char *name, const char *string)
WINPR_JSON_AddStringToObject.
Definition c-json.c:269
WINPR_API void WINPR_JSON_Delete(WINPR_JSON *item)
Delete a WinPR JSON wrapper object.
Definition c-json.c:103
WINPR_ATTR_NODISCARD WINPR_API const char * WINPR_JSON_GetStringValue(WINPR_JSON *item)
Return the String value of a JSON item.
Definition c-json.c:142
WINPR_API WINPR_JSON * WINPR_JSON_Parse(const char *value)
Parse a '\0' terminated JSON string.
Definition c-json.c:93
WINPR_ATTR_NODISCARD FREERDP_API const char * freerdp_settings_get_string(const rdpSettings *settings, FreeRDP_Settings_Keys_String id)
Returns a immutable string settings value.