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