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