FreeRDP
Loading...
Searching...
No Matches
sdl_aad_helper.cpp
1
21#include <cstring>
22#include <memory>
23#include <sstream>
24#include <string>
25
26#include <winpr/library.h>
27#include <winpr/path.h>
28#include <winpr/string.h>
29#include <freerdp/client.h>
30#include <freerdp/client/aad_helper.h>
31#include <freerdp/log.h>
32#include <freerdp/utils/aad.h>
33
34#include "sdl_aad_helper.hpp"
35
36#define TAG CLIENT_TAG("SDL.aadhelper")
37
38/* special /azure:auth-helper: value requesting the auto-probe order below, instead of a literal
39 * path - also what's used when the option is omitted entirely. */
40#define AAD_AUTH_HELPER_AUTODETECT "autodetect"
41
42SdlAadAuthHelper::SdlAadAuthHelper(AadAuthHelper* helper) : _helper(helper)
43{
44}
45
46SdlAadAuthHelper::~SdlAadAuthHelper()
47{
48 stop();
49}
50
51void SdlAadAuthHelper::stop()
52{
53 if (_helper)
54 {
55 aad_auth_helper_stop(_helper);
56 _helper = nullptr;
57 }
58}
59
60AadAuthHelper* SdlAadAuthHelper::get() const
61{
62 return _helper;
63}
64
65/* whether any auto-detectable helper was enabled at build time at all - see
66 * WITH_XDG_AAD_AUTH_HELPER / WITH_WEBVIEW_AAD_AUTH_HELPER / WITH_QT_AAD_AUTH_HELPER in
67 * client/common/CMakeLists.txt, propagated here as compile definitions by
68 * client/SDL/common/CMakeLists.txt. Guards kHelperCandidates below: with none of the three
69 * defined there's nothing to list, and a zero-size array isn't valid standard C++. */
70#if defined(WITH_XDG_AAD_AUTH_HELPER) || defined(WITH_WEBVIEW_AAD_AUTH_HELPER) || \
71 defined(WITH_QT_AAD_AUTH_HELPER)
72#define SDL_AAD_HELPER_HAVE_CANDIDATES 1
73
74namespace
75{
76 /* auto-pick order for /azure:auth-helper:autodetect (or the option omitted entirely): xdg-open
77 * first (drives the user's actual default browser, so it inherits whatever SSO session/cookies
78 * are already there instead of prompting again), then the embedded webview (lighter, native OS
79 * look), then Qt. */
80 constexpr const char* kHelperCandidates[] = {
81#if defined(WITH_XDG_AAD_AUTH_HELPER)
82 "freerdp-xdg-aad-helper",
83#endif
84#if defined(WITH_WEBVIEW_AAD_AUTH_HELPER)
85 "freerdp-webview-aad-helper",
86#endif
87#if defined(WITH_QT_AAD_AUTH_HELPER)
88 "freerdp-qt-aad-helper",
89#endif
90 };
91} // namespace
92#endif
93
94#if defined(SDL_AAD_HELPER_HAVE_CANDIDATES)
95/* directory this client binary itself lives in - where an installed (or freshly built) helper
96 * binary is expected to sit alongside it. */
97static std::string sdl_aad_helper_binary_dir()
98{
99 char path[4096] = {};
100 if (GetModuleFileNameA(nullptr, path, sizeof(path)) == 0)
101 {
102 WLog_ERR(TAG, "[aad-auth] GetModuleFileNameA failed");
103 return "";
104 }
105
106 char* sep = strrchr(path, '/');
107#ifdef _WIN32
108 char* sepWin = strrchr(path, '\\');
109 if (!sep || (sepWin && (sepWin > sep)))
110 sep = sepWin;
111#endif
112 if (!sep)
113 return "";
114 *sep = '\0';
115 return path;
116}
117#endif
118
119#if defined(SDL_AAD_HELPER_HAVE_CANDIDATES)
120static std::string sdl_aad_helper_path_for_binary(const std::string& dir, const char* binaryName)
121{
122 std::string path = dir;
123 path += "/";
124 path += binaryName;
125#ifdef _WIN32
126 path += ".exe";
127#endif
128 return path;
129}
130#endif
131
132/* /azure:auth-helper:autodetect (or the option omitted entirely): probe the well-known binaries
133 * in kHelperCandidates order and use whichever is actually present. */
134static std::string sdl_aad_helper_auto_locate()
135{
136#if defined(SDL_AAD_HELPER_HAVE_CANDIDATES)
137 auto dir = sdl_aad_helper_binary_dir();
138 if (dir.empty())
139 return "";
140
141 for (const auto& binaryName : kHelperCandidates)
142 {
143 auto path = sdl_aad_helper_path_for_binary(dir, binaryName);
144 if (PathFileExistsA(path.c_str()))
145 return path;
146 }
147#endif
148 return "";
149}
150
151/* @p helper is the caller's own per-connection storage slot (e.g. a member of its SdlContext) -
152 * this file never stores anything itself, so it stays usable as one binary shared between the
153 * SDL2 and SDL3 clients regardless of their (different) concrete SdlContext type. */
154static AadAuthHelper* sdl_aad_helper_get(rdpContext* context, SdlAadAuthHelperPtr& helper)
155{
156 if (helper)
157 return helper->get();
158
159 std::string path;
160 const char* source = nullptr;
161
162 const rdpSettings* settings = context->settings;
163 const char* fromSettings =
164 settings ? freerdp_settings_get_string(settings, FreeRDP_AadAuthHelper) : nullptr;
165 if (fromSettings && fromSettings[0] && (strcmp(fromSettings, AAD_AUTH_HELPER_AUTODETECT) != 0))
166 {
167 path = fromSettings;
168 source = "/azure:auth-helper:";
169 }
170
171 if (path.empty())
172 {
173 path = sdl_aad_helper_auto_locate();
174 source = "auto-detected";
175 }
176
177 if (path.empty())
178 {
179 WLog_ERR(TAG, "[aad-auth] could not determine expected helper binary location");
180 return nullptr;
181 }
182
183 if (!PathFileExistsA(path.c_str()))
184 {
185 WLog_ERR(
186 TAG,
187 "[aad-auth] helper binary not found at '%s' (from %s) - was FreeRDP built and "
188 "installed with -DWITH_XDG_AAD_AUTH_HELPER=ON, -DWITH_WEBVIEW_AAD_AUTH_HELPER=ON or "
189 "-DWITH_QT_AAD_AUTH_HELPER=ON? Falling back to manual copy/paste login",
190 path.c_str(), source);
191 return nullptr;
192 }
193
194 WLog_DBG(TAG, "[aad-auth] using helper path from %s: %s", source, path.c_str());
195 auto* raw = aad_auth_helper_start(path.c_str());
196 if (!raw)
197 {
198 WLog_ERR(TAG, "[aad-auth] failed to start '%s'", path.c_str());
199 return nullptr;
200 }
201
202 helper = std::make_shared<SdlAadAuthHelper>(raw);
203 return raw;
204}
205
206[[nodiscard]]
207static std::string sdl_aad_helper_extract_query_param(const std::string& url,
208 const std::string& name)
209{
210 auto qpos = url.find('?');
211 if (qpos == std::string::npos)
212 return "";
213
214 std::istringstream stream(url.substr(qpos + 1));
215 std::string pair;
216 while (std::getline(stream, pair, '&'))
217 {
218 auto eq = pair.find('=');
219 if (eq == std::string::npos)
220 continue;
221 if (pair.compare(0, eq, name) != 0)
222 continue;
223
224 auto value = pair.substr(eq + 1);
225 auto decoded = winpr_str_url_decode(value.c_str(), value.length());
226 std::string result = decoded ? decoded : "";
227 free(decoded);
228 return result;
229 }
230 return "";
231}
232
240static AadAuthHelperNavigateStatus
241sdl_aad_helper_navigate(rdpContext* context, SdlAadAuthHelperPtr& helper, const std::string& title,
242 const std::string& url, std::string& redirectUrl)
243{
244 auto redirectUri = sdl_aad_helper_extract_query_param(url, "redirect_uri");
245 if (redirectUri.empty())
246 {
247 WLog_ERR(TAG, "[aad-auth] url %s has no redirect_uri parameter", url.c_str());
248 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
249 }
250
251 auto* rawHelper = sdl_aad_helper_get(context, helper);
252 if (!rawHelper)
253 return AAD_AUTH_HELPER_NAVIGATE_ERROR;
254
255 char* out = nullptr;
256 const AadAuthHelperNavigateStatus status = aad_auth_helper_navigate(
257 rawHelper, title.c_str(), url.c_str(), redirectUri.c_str(), 180000, &out);
258 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
259 return status;
260
261 redirectUrl = out;
262 free(out);
263 return AAD_AUTH_HELPER_NAVIGATE_OK;
264}
265
266static BOOL sdl_aad_helper_get_rdsaad_access_token(freerdp* instance, SdlAadAuthHelperPtr& helper,
267 const char* scope, const char* req_cnf,
268 char** token)
269{
270 WINPR_ASSERT(instance);
271 WINPR_ASSERT(scope);
272 WINPR_ASSERT(req_cnf);
273 WINPR_ASSERT(token);
274
275 auto context = instance->context;
276 WINPR_ASSERT(context);
277 WINPR_ASSERT(context->settings);
278
279 std::shared_ptr<char> request(
280 freerdp_client_get_aad_url(reinterpret_cast<rdpClientContext*>(instance->context),
281 FREERDP_CLIENT_AAD_AUTH_REQUEST, scope),
282 winpr_zfree);
283
284 std::string redirectUrl;
285 const AadAuthHelperNavigateStatus status = sdl_aad_helper_navigate(
286 context, helper, "FreeRDP WebView - AAD access token", request.get(), redirectUrl);
287 if (status == AAD_AUTH_HELPER_NAVIGATE_CANCELLED)
288 {
289 WLog_INFO(TAG, "[aad-auth] user cancelled the authentication");
290 return FALSE;
291 }
292 if (status == AAD_AUTH_HELPER_NAVIGATE_TIMEOUT)
293 {
294 WLog_ERR(TAG, "[aad-auth] authentication timed out");
295 return FALSE;
296 }
297 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
298 return client_cli_get_access_token(instance, ACCESS_TOKEN_TYPE_AAD, token, 2, scope,
299 req_cnf);
300
301 std::unique_ptr<char, void (*)(char*)> code(
302 freerdp_client_extract_aad_code(reinterpret_cast<rdpClientContext*>(instance->context),
303 redirectUrl.c_str(), redirectUrl.size()),
304 winpr_zfree);
305
306 if (!code)
307 return client_cli_get_access_token(instance, ACCESS_TOKEN_TYPE_AAD, token, 2, scope,
308 req_cnf);
309
310 std::shared_ptr<char> token_request(
311 freerdp_client_get_aad_url(reinterpret_cast<rdpClientContext*>(instance->context),
312 FREERDP_CLIENT_AAD_TOKEN_REQUEST, scope, code.get(), req_cnf),
313 winpr_zfree);
314 return client_common_get_access_token(instance, token_request.get(), token);
315}
316
317static BOOL sdl_aad_helper_get_avd_access_token(freerdp* instance, SdlAadAuthHelperPtr& helper,
318 char** token)
319{
320 WINPR_ASSERT(token);
321 WINPR_ASSERT(instance);
322 WINPR_ASSERT(instance->context);
323
324 std::shared_ptr<char> request(
325 freerdp_client_get_aad_url(reinterpret_cast<rdpClientContext*>(instance->context),
326 FREERDP_CLIENT_AAD_AVD_AUTH_REQUEST),
327 winpr_zfree);
328
329 std::string redirectUrl;
330 const AadAuthHelperNavigateStatus status =
331 sdl_aad_helper_navigate(instance->context, helper, "FreeRDP WebView - AVD access token",
332 request.get(), redirectUrl);
333 if (status == AAD_AUTH_HELPER_NAVIGATE_CANCELLED)
334 {
335 WLog_INFO(TAG, "[aad-auth] user cancelled the authentication");
336 return FALSE;
337 }
338 if (status == AAD_AUTH_HELPER_NAVIGATE_TIMEOUT)
339 {
340 WLog_ERR(TAG, "[aad-auth] authentication timed out");
341 return FALSE;
342 }
343 if (status != AAD_AUTH_HELPER_NAVIGATE_OK)
344 return client_cli_get_access_token(instance, ACCESS_TOKEN_TYPE_AVD, token, 0);
345
346 std::unique_ptr<char, void (*)(char*)> code(
347 freerdp_client_extract_aad_code(reinterpret_cast<rdpClientContext*>(instance->context),
348 redirectUrl.c_str(), redirectUrl.size()),
349 winpr_zfree);
350 if (!code)
351 return client_cli_get_access_token(instance, ACCESS_TOKEN_TYPE_AVD, token, 0);
352
353 std::shared_ptr<char> token_request(
354 freerdp_client_get_aad_url(reinterpret_cast<rdpClientContext*>(instance->context),
355 FREERDP_CLIENT_AAD_AVD_TOKEN_REQUEST, code.get()),
356 winpr_zfree);
357 return client_common_get_access_token(instance, token_request.get(), token);
358}
359
360BOOL sdl_aad_helper_get_access_token_v(freerdp* instance, SdlAadAuthHelperPtr& helper,
361 AccessTokenType tokenType, char** token, size_t count,
362 va_list args)
363{
364 WINPR_ASSERT(instance);
365 WINPR_ASSERT(token);
366 switch (tokenType)
367 {
368 case ACCESS_TOKEN_TYPE_AAD:
369 {
370 if (count < 2)
371 {
372 WLog_ERR(TAG,
373 "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
374 ", aborting",
375 count);
376 return FALSE;
377 }
378 else if (count > 2)
379 WLog_WARN(TAG,
380 "ACCESS_TOKEN_TYPE_AAD expected 2 additional arguments, but got %" PRIuz
381 ", ignoring",
382 count);
383 const char* scope = va_arg(args, const char*);
384 const char* req_cnf = va_arg(args, const char*);
385 return sdl_aad_helper_get_rdsaad_access_token(instance, helper, scope, req_cnf, token);
386 }
387 case ACCESS_TOKEN_TYPE_AVD:
388 if (count != 0)
389 WLog_WARN(TAG,
390 "ACCESS_TOKEN_TYPE_AVD expected 0 additional arguments, but got %" PRIuz
391 ", ignoring",
392 count);
393 return sdl_aad_helper_get_avd_access_token(instance, helper, token);
394 default:
395 WLog_ERR(TAG, "Unexpected value for AccessTokenType [%" PRIu32 "], aborting",
396 tokenType);
397 return FALSE;
398 }
399}
400
401BOOL sdl_aad_helper_get_access_token(freerdp* instance, SdlAadAuthHelperPtr& helper,
402 AccessTokenType tokenType, char** token, size_t count, ...)
403{
404 va_list ap = {};
405 va_start(ap, count);
406 const BOOL rc =
407 sdl_aad_helper_get_access_token_v(instance, helper, tokenType, token, count, ap);
408 va_end(ap);
409 return rc;
410}
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.