diff --git a/configure.ac b/configure.ac index 7d0f00045e..66ad262005 100644 --- a/configure.ac +++ b/configure.ac @@ -1156,6 +1156,21 @@ then [AC_MSG_RESULT([no])]) fi +if test "x${have_freerdp}" = "xyes" +then + AC_CHECK_DECLS([FreeRDP_AadSecurity], + [have_freerdp_aad=yes], [have_freerdp_aad=no], + [#include ]) + + if test "x${have_freerdp_aad}" = "xyes" + then + PKG_CHECK_MODULES([CURL], [libcurl], + [AC_DEFINE([HAVE_FREERDP_AAD_SUPPORT],, + [Defined if FreeRDP supports Azure AD authentication and libcurl is available])], + [AC_MSG_WARN([libcurl not found - Azure AD authentication disabled])]) + fi +fi + # Restore CPPFLAGS, removing FreeRDP-specific options needed for testing CPPFLAGS="$OLDCPPFLAGS" diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index dc9638e305..1565be1a5d 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -38,6 +38,7 @@ nodist_libguac_client_rdp_la_SOURCES = \ _generated_keymaps.c libguac_client_rdp_la_SOURCES = \ + aad.c \ argv.c \ beep.c \ channels/audio-input/audio-buffer.c \ @@ -84,6 +85,7 @@ libguac_client_rdp_la_SOURCES = \ user.c noinst_HEADERS = \ + aad.h \ argv.h \ beep.h \ channels/audio-input/audio-buffer.h \ @@ -141,7 +143,8 @@ libguac_client_rdp_la_LDFLAGS = \ -version-info 0:0:0 \ @CAIRO_LIBS@ \ @PTHREAD_LIBS@ \ - @RDP_LIBS@ + @RDP_LIBS@ \ + @CURL_LIBS@ libguac_client_rdp_la_LIBADD = \ @COMMON_LTLIB@ \ diff --git a/src/protocols/rdp/aad.c b/src/protocols/rdp/aad.c new file mode 100644 index 0000000000..f6a512535f --- /dev/null +++ b/src/protocols/rdp/aad.c @@ -0,0 +1,1072 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "config.h" + +#ifdef HAVE_FREERDP_AAD_SUPPORT + +#include "aad.h" + +#include +#include +#include + +#include +#include +#include + +/** + * Azure AD OAuth2 token endpoint URL format. + * The %s placeholder is replaced with the tenant ID. + */ +#define GUAC_AAD_TOKEN_ENDPOINT \ + "https://login.microsoftonline.com/%s/oauth2/v2.0/token" + +/** + * Azure AD OAuth2 authorization endpoint URL format. + * The %s placeholder is replaced with the tenant ID. + */ +#define GUAC_AAD_AUTHORIZE_ENDPOINT \ + "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize" + +/** + * The native client redirect URI used for the authorization code flow. + * This is a special Microsoft-provided redirect URI for non-web applications. + */ +#define GUAC_AAD_NATIVE_REDIRECT_URI \ + "https://login.microsoftonline.com/common/oauth2/nativeclient" + +/** + * Maximum size for the login page HTML response. + */ +#define GUAC_AAD_LOGIN_PAGE_MAX_SIZE (64 * 1024) + +/** + * HTTP request timeout in seconds. + */ +#define GUAC_AAD_HTTP_TIMEOUT_SECONDS 30 + +/** + * User-Agent string sent with all HTTP requests to Microsoft login endpoints. + * A browser-like UA is required to avoid "unsupported browser" responses. + */ +#define GUAC_AAD_USER_AGENT \ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \ + "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" + +/** + * HTTP response structure for AAD requests. + */ +typedef struct guac_rdp_aad_response { + + /** + * The response body data. + */ + char* data; + + /** + * The current size of the response data. + */ + size_t size; + +} guac_rdp_aad_response; + +/** + * Callback function for libcurl to write received HTTP data into a + * guac_rdp_aad_response buffer. + * + * @param contents + * Pointer to the received data. + * + * @param size + * Size of each data element. + * + * @param nmemb + * Number of data elements. + * + * @param userp + * User-provided pointer (guac_rdp_aad_response structure). + * + * @return + * The number of bytes processed. + */ +static size_t guac_rdp_aad_write_callback(void* contents, size_t size, + size_t nmemb, void* userp) { + + size_t total_size = size * nmemb; + guac_rdp_aad_response* response = (guac_rdp_aad_response*) userp; + + /* Reject responses that exceed the maximum login page size */ + if (response->size + total_size > GUAC_AAD_LOGIN_PAGE_MAX_SIZE) + return 0; + + /* Copy data into response buffer and null-terminate */ + memcpy(response->data + response->size, contents, total_size); + response->size += total_size; + response->data[response->size] = '\0'; + + return total_size; +} + +/** + * URL-encodes a string for use in HTTP POST data or query parameters. + * + * @param curl + * The CURL handle to use for encoding. + * + * @param str + * The string to encode. + * + * @return + * A newly allocated URL-encoded string, or NULL on error. The caller + * must free this string using curl_free(). + */ +static char* guac_rdp_aad_urlencode(CURL* curl, const char* str) { + if (str == NULL) + return NULL; + return curl_easy_escape(curl, str, strlen(str)); +} + +/** + * Allocates and initializes a new guac_rdp_aad_response structure with a + * fixed buffer large enough to hold the maximum allowed response. + * + * @return + * A newly allocated response structure, or NULL on allocation failure. + * The caller must free this with guac_rdp_aad_response_free(). + */ +static guac_rdp_aad_response* guac_rdp_aad_response_alloc(void) { + + guac_rdp_aad_response* response = + guac_mem_zalloc(sizeof(guac_rdp_aad_response)); + + if (response == NULL) + return NULL; + + /* Allocate the maximum allowed size upfront so the write callback + * never needs to reallocate */ + response->data = guac_mem_alloc(GUAC_AAD_LOGIN_PAGE_MAX_SIZE + 1); + + if (response->data == NULL) { + guac_mem_free(response); + return NULL; + } + + response->data[0] = '\0'; + + return response; +} + +/** + * Extracts a string value from the $Config JavaScript object embedded in + * the Microsoft login page HTML. Searches for the pattern "key":" and + * returns the value up to the next unescaped double-quote. + * + * @param html + * The HTML string to search. + * + * @param key + * The JSON key name to find (without quotes). + * + * @return + * A newly allocated string containing the extracted value, or NULL if + * the key was not found. The caller must free with guac_mem_free(). + */ +static char* guac_rdp_aad_extract_config_value(const char* html, + const char* key) { + + if (html == NULL || key == NULL) + return NULL; + + char pattern[256]; + snprintf(pattern, sizeof(pattern), "\"%s\":\"", key); + + const char* value_start = strstr(html, pattern); + if (value_start == NULL) + return NULL; + + value_start += strlen(pattern); + + /* Find closing quote, skipping escaped characters */ + const char* value_end = value_start; + while (*value_end != '\0') { + if (*value_end == '\\' && *(value_end + 1) != '\0') { + /* Skip escaped character */ + value_end += 2; + continue; + } + if (*value_end == '"') + break; + value_end++; + } + + if (*value_end != '"') + return NULL; + + size_t value_len = value_end - value_start; + return guac_strndup(value_start, value_len); +} + +char* guac_rdp_percent_decode(const char* str) { + + if (str == NULL) + return NULL; + + size_t len = strlen(str); + char* decoded = guac_mem_alloc(len + 1); + size_t out_pos = 0; + + for (size_t i = 0; i < len; i++) { + if (str[i] == '%' && i + 2 < len) { + char hex[3] = { str[i + 1], str[i + 2], '\0' }; + char* hex_end; + long byte_val = strtol(hex, &hex_end, 16); + if (hex_end == hex + 2) { + decoded[out_pos++] = (char) byte_val; + i += 2; + continue; + } + } + decoded[out_pos++] = str[i]; + } + + decoded[out_pos] = '\0'; + return decoded; +} + +/** + * Parses the JSON response from a token exchange request and extracts the + * access token. If the response contains an error_description field instead, + * that error is logged. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param json_response + * The raw JSON response body from the token endpoint. + * + * @return + * A newly allocated string containing the access token, or NULL if + * parsing failed or the response contained an error. The caller must + * free the returned string with guac_mem_free(). + */ +static char* guac_rdp_aad_parse_token_response(guac_client* client, + const char* json_response) { + + if (json_response == NULL) + return NULL; + + /* Look for access_token in the JSON response */ + const char* token_key = "\"access_token\""; + const char* token_pos = strstr(json_response, token_key); + + if (token_pos == NULL) { + + /* Log error description if present instead */ + const char* error_desc_key = "\"error_description\""; + const char* error_desc_pos = strstr(json_response, error_desc_key); + if (error_desc_pos != NULL) + error_desc_pos = strchr(error_desc_pos + + strlen(error_desc_key), '"'); + if (error_desc_pos != NULL) { + error_desc_pos++; + const char* error_end = strchr(error_desc_pos, '"'); + if (error_end != NULL && error_end > error_desc_pos) + guac_client_log(client, GUAC_LOG_ERROR, + "AAD authentication error: %.*s", + (int)(error_end - error_desc_pos), error_desc_pos); + } + + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: No access_token found in response"); + return NULL; + } + + /* Extract token value from JSON */ + const char* value_start = strchr(token_pos + strlen(token_key), '"'); + if (value_start == NULL) + return NULL; + + value_start++; + + const char* value_end = strchr(value_start, '"'); + if (value_end == NULL) + return NULL; + + size_t token_length = value_end - value_start; + if (token_length == 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Empty access token in response"); + return NULL; + } + + return guac_strndup(value_start, token_length); +} + +/** + * Frees a guac_rdp_aad_response structure and its associated data buffer. + * + * @param response + * The response structure to free, or NULL (in which case this function + * is a no-op). + */ +static void guac_rdp_aad_response_free(guac_rdp_aad_response* response) { + if (response == NULL) + return; + + guac_mem_free(response->data); + guac_mem_free(response); +} + +/** + * Builds the OAuth2 authorization URL for the Azure AD login endpoint, + * including all required query parameters. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param params + * The AAD authentication parameters containing tenant ID, client ID, + * and scope. + * + * @param url_buffer + * Buffer to receive the constructed authorization URL. + * + * @param buffer_size + * Size of url_buffer in bytes. + * + * @return + * Zero on success, non-zero if the URL could not be constructed. + */ +static int guac_rdp_aad_build_auth_url(guac_client* client, + guac_rdp_aad_params* params, char* url_buffer, size_t buffer_size) { + + CURL* curl = curl_easy_init(); + if (curl == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to initialize curl for URL building"); + return 1; + } + + /* URL-encode query parameters */ + char* encoded_client_id = guac_rdp_aad_urlencode(curl, params->client_id); + char* encoded_scope = guac_rdp_aad_urlencode(curl, params->scope); + char* encoded_redirect_uri = guac_rdp_aad_urlencode(curl, + GUAC_AAD_NATIVE_REDIRECT_URI); + + if (!encoded_client_id || !encoded_scope || !encoded_redirect_uri) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to URL-encode authorization parameters"); + + if (encoded_client_id) + curl_free(encoded_client_id); + if (encoded_scope) + curl_free(encoded_scope); + if (encoded_redirect_uri) + curl_free(encoded_redirect_uri); + curl_easy_cleanup(curl); + return 1; + } + + /* Build authorization URL with query parameters */ + char authorize_url[512]; + snprintf(authorize_url, sizeof(authorize_url), + GUAC_AAD_AUTHORIZE_ENDPOINT, params->tenant_id); + + int written = snprintf(url_buffer, buffer_size, + "%s?client_id=%s" + "&response_type=code" + "&redirect_uri=%s" + "&scope=%s" + "&response_mode=query", + authorize_url, + encoded_client_id, + encoded_redirect_uri, + encoded_scope); + + curl_free(encoded_client_id); + curl_free(encoded_scope); + curl_free(encoded_redirect_uri); + curl_easy_cleanup(curl); + + if (written < 0 || (size_t) written >= buffer_size) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Authorization URL exceeds buffer size"); + return 1; + } + + return 0; +} + +/** + * Extracts the authorization code from a redirect URL returned after + * successful authentication. If the URL contains an error response instead, + * the error description is logged. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param url + * The redirect URL containing either a "code=" parameter on success + * or an "error=" parameter on failure. + * + * @return + * A newly allocated string containing the authorization code, or NULL + * if the code could not be extracted. The caller must free the returned + * string with guac_mem_free(). + */ +static char* guac_rdp_aad_extract_auth_code(guac_client* client, + const char* url) { + + if (url == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Cannot extract auth code from NULL URL"); + return NULL; + } + + /* Check for error response in the URL */ + const char* error_pos = strstr(url, "error="); + if (error_pos != NULL) { + const char* error_desc = strstr(url, "error_description="); + if (error_desc != NULL) { + error_desc += strlen("error_description="); + const char* error_end = strchr(error_desc, '&'); + size_t error_len = error_end ? + (size_t)(error_end - error_desc) : strlen(error_desc); + + if (error_len > 0) { + char* decoded_error = guac_rdp_percent_decode(error_desc); + if (decoded_error) { + + /* Truncate at first & if present */ + char* separator = strchr(decoded_error, '&'); + if (separator) + *separator = '\0'; + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Authorization error: %s", decoded_error); + guac_mem_free(decoded_error); + } + } + } + return NULL; + } + + /* Look for "code=" in the URL */ + const char* code_pos = strstr(url, "code="); + if (code_pos == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: No authorization code found in redirect URL"); + return NULL; + } + + code_pos += strlen("code="); + + const char* code_end = strchr(code_pos, '&'); + size_t code_len = code_end ? + (size_t)(code_end - code_pos) : strlen(code_pos); + + if (code_len == 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Empty authorization code in redirect URL"); + return NULL; + } + + return guac_strndup(code_pos, code_len); +} + +/** + * Exchanges an authorization code for an access token by POSTing to the + * Azure AD token endpoint. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param params + * The AAD authentication parameters containing tenant ID, client ID, + * and scope. + * + * @param auth_code + * The authorization code obtained from the login redirect. + * + * @return + * A newly allocated string containing the access token, or NULL if + * the exchange failed. The caller must free the returned string with + * guac_mem_free(). + */ +static char* guac_rdp_aad_exchange_code_for_token(guac_client* client, + guac_rdp_aad_params* params, const char* auth_code) { + + CURL* curl = NULL; + char* token = NULL; + char* post_data = NULL; + + curl = curl_easy_init(); + if (curl == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to initialize curl for token exchange"); + return NULL; + } + + guac_rdp_aad_response* response = guac_rdp_aad_response_alloc(); + if (response == NULL) { + curl_easy_cleanup(curl); + return NULL; + } + + char token_url[512]; + snprintf(token_url, sizeof(token_url), GUAC_AAD_TOKEN_ENDPOINT, + params->tenant_id); + + guac_client_log(client, GUAC_LOG_DEBUG, + "AAD: Exchanging authorization code for access token"); + + /* URL-encode token exchange parameters */ + char* encoded_client_id = guac_rdp_aad_urlencode(curl, params->client_id); + char* encoded_code = guac_rdp_aad_urlencode(curl, auth_code); + char* encoded_redirect_uri = guac_rdp_aad_urlencode(curl, + GUAC_AAD_NATIVE_REDIRECT_URI); + char* encoded_scope = guac_rdp_aad_urlencode(curl, params->scope); + char* encoded_req_cnf = params->req_cnf ? + guac_rdp_aad_urlencode(curl, params->req_cnf) : NULL; + + if (!encoded_client_id || !encoded_code || !encoded_redirect_uri + || !encoded_scope) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to URL-encode token exchange parameters"); + goto cleanup; + } + + /* Build token exchange POST body */ + size_t post_data_size = 1024 + + strlen(encoded_client_id) + strlen(encoded_code) + + strlen(encoded_redirect_uri) + strlen(encoded_scope) + + (encoded_req_cnf ? strlen(encoded_req_cnf) : 0); + + post_data = guac_mem_alloc(post_data_size); + + int written = snprintf(post_data, post_data_size, + "grant_type=authorization_code" + "&client_id=%s" + "&code=%s" + "&redirect_uri=%s" + "&scope=%s", + encoded_client_id, + encoded_code, + encoded_redirect_uri, + encoded_scope); + + /* Append req_cnf (Proof-of-Possession) if provided by FreeRDP */ + if (encoded_req_cnf && written > 0 + && (size_t) written < post_data_size - 1) { + snprintf(post_data + written, post_data_size - written, + "&req_cnf=%s", encoded_req_cnf); + } + + /* Configure and send the token request */ + curl_easy_setopt(curl, CURLOPT_URL, token_url); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + guac_rdp_aad_write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); + curl_easy_setopt(curl, CURLOPT_USERAGENT, GUAC_AAD_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, + (long) GUAC_AAD_HTTP_TIMEOUT_SECONDS); + + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, + "Content-Type: application/x-www-form-urlencoded"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Token exchange HTTP request failed: %s", + curl_easy_strerror(res)); + goto cleanup; + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code != 200) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Token exchange failed with HTTP %ld", http_code); + } + + /* Parse access token from response */ + token = guac_rdp_aad_parse_token_response(client, response->data); + +cleanup: + if (headers) + curl_slist_free_all(headers); + if (encoded_client_id) + curl_free(encoded_client_id); + if (encoded_code) + curl_free(encoded_code); + if (encoded_redirect_uri) + curl_free(encoded_redirect_uri); + if (encoded_scope) + curl_free(encoded_scope); + if (encoded_req_cnf) + curl_free(encoded_req_cnf); + + guac_mem_free(post_data); + curl_easy_cleanup(curl); + guac_rdp_aad_response_free(response); + + return token; +} + +/** + * Calls the Microsoft GetCredentialType API to update server-side session + * state and obtain a fresh flow token for credential submission. Without + * this intermediate call, the credential POST returns a ConvergedError. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param curl + * An initialized CURL handle with cookies enabled. + * + * @param params + * The AAD authentication parameters (username, tenant_id, etc.). + * + * @param auth_url + * The original authorization URL, used as the Referer header. + * + * @param flow_token + * Pointer to the current flow token string. On success, the old token + * is freed and replaced with the updated token from the API response. + * + * @param ctx + * The session context value from the login page $Config. + * + * @param api_canary + * The API canary token from the login page, or NULL if not available. + */ +static void guac_rdp_aad_get_credential_type(guac_client* client, + CURL* curl, guac_rdp_aad_params* params, const char* auth_url, + char** flow_token, const char* ctx, const char* api_canary) { + + guac_client_log(client, GUAC_LOG_DEBUG, + "AAD: Calling GetCredentialType API"); + + char gct_url[512]; + snprintf(gct_url, sizeof(gct_url), + "https://login.microsoftonline.com/%s/GetCredentialType?mkt=en", + params->tenant_id); + + /* Build GetCredentialType JSON request body */ + size_t gct_body_size = 256 + strlen(*flow_token) + strlen(ctx) + + strlen(params->username); + char* gct_body = guac_mem_alloc(gct_body_size); + snprintf(gct_body, gct_body_size, + "{\"username\":\"%s\"," + "\"originalRequest\":\"%s\"," + "\"flowToken\":\"%s\"}", + params->username, ctx, *flow_token); + + /* Set required headers */ + struct curl_slist* gct_headers = NULL; + gct_headers = curl_slist_append(gct_headers, + "Content-Type: application/json"); + gct_headers = curl_slist_append(gct_headers, + "Origin: https://login.microsoftonline.com"); + + /* Add API canary as header if available */ + if (api_canary != NULL) { + char canary_header[4096]; + snprintf(canary_header, sizeof(canary_header), + "canary: %s", api_canary); + gct_headers = curl_slist_append(gct_headers, canary_header); + } + + char referer_header[2048]; + snprintf(referer_header, sizeof(referer_header), "Referer: %s", auth_url); + gct_headers = curl_slist_append(gct_headers, referer_header); + + guac_rdp_aad_response* gct_response = guac_rdp_aad_response_alloc(); + if (gct_response == NULL) { + curl_slist_free_all(gct_headers); + guac_mem_free(gct_body); + return; + } + + curl_easy_setopt(curl, CURLOPT_URL, gct_url); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, gct_body); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, gct_headers); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, gct_response); + + CURLcode res = curl_easy_perform(curl); + + curl_slist_free_all(gct_headers); + + if (res == CURLE_OK && gct_response->data != NULL) { + + /* Extract the updated flow token from the JSON response */ + const char* flow_token_key = "\"FlowToken\":\""; + const char* flow_token_start = strstr(gct_response->data, + flow_token_key); + if (flow_token_start != NULL) { + flow_token_start += strlen(flow_token_key); + const char* flow_token_end = strchr(flow_token_start, '"'); + if (flow_token_end != NULL) { + guac_mem_free(*flow_token); + size_t flow_token_len = flow_token_end - flow_token_start; + *flow_token = guac_strndup(flow_token_start, flow_token_len); + } + } + } + + /* GetCredentialType call failed */ + else { + guac_client_log(client, GUAC_LOG_WARNING, + "AAD: GetCredentialType call failed, continuing with " + "original flow token"); + } + + guac_rdp_aad_response_free(gct_response); + guac_mem_free(gct_body); +} + +/** + * Performs the full automated browser-based login flow against the Microsoft + * login endpoint. Fetches the login page, parses session tokens from $Config, + * calls GetCredentialType, and posts credentials to obtain an authorization + * code. + * + * @param client + * The guac_client associated with the current RDP connection. + * + * @param auth_url + * The full authorization URL to start the login flow. + * + * @param params + * The AAD authentication parameters including username and password. + * + * @return + * A newly allocated string containing the authorization code, or NULL + * if login failed. The caller must free the returned string with + * guac_mem_free(). + */ +static char* guac_rdp_aad_automated_login(guac_client* client, + const char* auth_url, guac_rdp_aad_params* params) { + + char* auth_code = NULL; + CURL* curl = NULL; + char* flow_token = NULL; + char* ctx = NULL; + char* post_url = NULL; + char* canary = NULL; + char* post_data = NULL; + + curl = curl_easy_init(); + if (curl == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to initialize curl for automated login"); + return NULL; + } + + curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); + curl_easy_setopt(curl, CURLOPT_USERAGENT, GUAC_AAD_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, + (long) GUAC_AAD_HTTP_TIMEOUT_SECONDS); + + /* Step 1: GET the authorization URL to get the login page */ + + guac_client_log(client, GUAC_LOG_DEBUG, + "AAD: Fetching login page from authorization URL"); + + guac_rdp_aad_response* login_page = guac_rdp_aad_response_alloc(); + if (login_page == NULL) + goto cleanup; + + curl_easy_setopt(curl, CURLOPT_URL, auth_url); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + guac_rdp_aad_write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, login_page); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to fetch login page: %s", + curl_easy_strerror(res)); + goto cleanup; + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code != 200) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Login page returned HTTP %ld", http_code); + goto cleanup; + } + + /* Step 2: Parse $Config from the login page HTML */ + + guac_client_log(client, GUAC_LOG_DEBUG, + "AAD: Parsing $Config from login page (%zu bytes)", + login_page->size); + + flow_token = guac_rdp_aad_extract_config_value(login_page->data, "sFT"); + ctx = guac_rdp_aad_extract_config_value(login_page->data, "sCtx"); + post_url = guac_rdp_aad_extract_config_value(login_page->data, "urlPost"); + canary = guac_rdp_aad_extract_config_value(login_page->data, "canary"); + + /* Extract API canary (used for JSON API calls like GetCredentialType) */ + char* api_canary = guac_rdp_aad_extract_config_value(login_page->data, + "apiCanary"); + + if (flow_token == NULL || ctx == NULL || post_url == NULL + || canary == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to parse login page $Config " + "(sFT=%s, sCtx=%s, urlPost=%s, canary=%s)", + flow_token ? "found" : "MISSING", + ctx ? "found" : "MISSING", + post_url ? "found" : "MISSING", + canary ? "found" : "MISSING"); + guac_mem_free(api_canary); + goto cleanup; + } + + /* Update server-side session state and get a fresh flow token */ + guac_rdp_aad_get_credential_type(client, curl, params, auth_url, + &flow_token, ctx, api_canary); + guac_mem_free(api_canary); + + /* Step 3: POST credentials */ + + guac_client_log(client, GUAC_LOG_DEBUG, + "AAD: Posting credentials to login endpoint"); + + /* URL-encode credential parameters */ + char* encoded_login = guac_rdp_aad_urlencode(curl, params->username); + char* encoded_passwd = guac_rdp_aad_urlencode(curl, params->password); + char* encoded_ctx = guac_rdp_aad_urlencode(curl, ctx); + char* encoded_flowtoken = guac_rdp_aad_urlencode(curl, flow_token); + char* encoded_canary = guac_rdp_aad_urlencode(curl, canary); + + if (!encoded_login || !encoded_passwd || !encoded_ctx + || !encoded_flowtoken || !encoded_canary) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to URL-encode login credentials"); + if (encoded_login) + curl_free(encoded_login); + if (encoded_passwd) + curl_free(encoded_passwd); + if (encoded_ctx) + curl_free(encoded_ctx); + if (encoded_flowtoken) + curl_free(encoded_flowtoken); + if (encoded_canary) + curl_free(encoded_canary); + goto cleanup; + } + + size_t post_data_size = 1024 + + (strlen(encoded_login) * 2) + + strlen(encoded_passwd) + + strlen(encoded_ctx) + strlen(encoded_flowtoken) + + strlen(encoded_canary); + + post_data = guac_mem_alloc(post_data_size); + + /* Build credential POST body. Both "login" and "loginfmt" are required + * by Microsoft. The canary, ctx, and flowtoken are CSRF/session tokens + * from the login page $Config. type=11 indicates password auth. */ + snprintf(post_data, post_data_size, + "login=%s" + "&loginfmt=%s" + "&passwd=%s" + "&canary=%s" + "&ctx=%s" + "&flowtoken=%s" + "&type=11", + encoded_login, + encoded_login, + encoded_passwd, + encoded_canary, + encoded_ctx, + encoded_flowtoken); + + curl_free(encoded_login); + curl_free(encoded_passwd); + curl_free(encoded_ctx); + curl_free(encoded_flowtoken); + curl_free(encoded_canary); + + /* Reset response buffer for the credential POST */ + guac_rdp_aad_response_free(login_page); + login_page = guac_rdp_aad_response_alloc(); + if (login_page == NULL) + goto cleanup; + + /* Configure credential POST request */ + curl_easy_setopt(curl, CURLOPT_URL, post_url); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, login_page); + + /* Set headers that the browser normally sends. Microsoft's login endpoint + * checks Origin and Referer for CSRF protection beyond the canary token. */ + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, + "Content-Type: application/x-www-form-urlencoded"); + headers = curl_slist_append(headers, + "Origin: https://login.microsoftonline.com"); + + char referer_header[2048]; + snprintf(referer_header, sizeof(referer_header), "Referer: %s", auth_url); + headers = curl_slist_append(headers, referer_header); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + res = curl_easy_perform(curl); + + if (headers) + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Credential POST failed: %s", + curl_easy_strerror(res)); + goto cleanup; + } + + /* Step 4: Check the result of the credential POST */ + + char* effective_url = NULL; + curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url); + + guac_client_log(client, GUAC_LOG_DEBUG, + "AAD: Credential POST redirected to: %s", + effective_url ? effective_url : "(NULL)"); + + if (effective_url != NULL + && strncmp(effective_url, GUAC_AAD_NATIVE_REDIRECT_URI, + strlen(GUAC_AAD_NATIVE_REDIRECT_URI)) == 0) { + + auth_code = guac_rdp_aad_extract_auth_code(client, effective_url); + } + + /* Credential POST did not redirect to the native client URI */ + else { + + /* Log any error from the effective URL */ + if (effective_url != NULL + && strstr(effective_url, "error=") != NULL) + guac_rdp_aad_extract_auth_code(client, effective_url); + + /* Check for error code in the response body */ + if (login_page->data != NULL) { + + char* error_code = guac_rdp_aad_extract_config_value( + login_page->data, "sErrorCode"); + + if (error_code != NULL && strlen(error_code) > 0 + && strcmp(error_code, "0") != 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Login failed with error code: %s", + error_code); + } + + guac_mem_free(error_code); + } + + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Automated login failed - did not reach " + "redirect URI"); + } + +cleanup: + guac_mem_free(flow_token); + guac_mem_free(ctx); + guac_mem_free(post_url); + guac_mem_free(canary); + guac_mem_free(post_data); + + guac_rdp_aad_response_free(login_page); + curl_easy_cleanup(curl); + + return auth_code; +} + +char* guac_rdp_aad_get_token_authcode(guac_client* client, + guac_rdp_aad_params* params) { + + /* Require client_id, tenant_id, username, password, and scope */ + if (params == NULL || params->client_id == NULL + || params->tenant_id == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Missing required parameters (client_id and " + "tenant_id) for authorization code flow"); + return NULL; + } + + if (params->username == NULL || params->password == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Username and password are required for " + "authorization code flow"); + return NULL; + } + + if (params->scope == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Scope is required for authorization code flow"); + return NULL; + } + + /* Step 1: Build the authorization URL */ + char auth_url[2048]; + if (guac_rdp_aad_build_auth_url(client, params, + auth_url, sizeof(auth_url)) != 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to build authorization URL"); + return NULL; + } + + /* Step 2: Automated login to get the authorization code */ + guac_client_log(client, GUAC_LOG_INFO, + "AAD: Starting automated authorization code flow " + "for user: %s", params->username); + + char* auth_code = guac_rdp_aad_automated_login(client, auth_url, params); + + if (auth_code == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to obtain authorization code"); + return NULL; + } + + /* Step 3: Exchange the code for an access token */ + char* token = guac_rdp_aad_exchange_code_for_token(client, params, + auth_code); + + guac_mem_free(auth_code); + + return token; +} + +#endif /* HAVE_FREERDP_AAD_SUPPORT */ diff --git a/src/protocols/rdp/aad.h b/src/protocols/rdp/aad.h new file mode 100644 index 0000000000..a787abf573 --- /dev/null +++ b/src/protocols/rdp/aad.h @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef GUAC_RDP_AAD_H +#define GUAC_RDP_AAD_H + +#include + +/** + * Default tenant ID. The "common" endpoint supports multi-tenant + * authentication for both organizational and personal accounts. + */ +#define GUAC_AAD_DEFAULT_TENANT_ID "common" + +/** + * Azure AD authentication parameters used across all AAD auth flows. + */ +typedef struct guac_rdp_aad_params { + + /** + * The Azure AD tenant ID (or "common" for multi-tenant apps). + */ + char* tenant_id; + + /** + * The application (client) ID from Azure AD app registration. + */ + char* client_id; + + /** + * The username (email) for authentication. + */ + char* username; + + /** + * The password for authentication. + */ + char* password; + + /** + * The OAuth2 scope to request. + */ + char* scope; + + /** + * The Proof-of-Possession key confirmation parameter (req_cnf) provided + * by FreeRDP's AAD layer. This is a base64url-encoded JSON string + * containing the key ID (kid) derived from the POP RSA key pair. Azure AD + * uses this to bind the access token to the key. May be NULL if POP is + * not required. + */ + char* req_cnf; + +} guac_rdp_aad_params; + +/** + * Decodes a percent-encoded (URL-encoded) string. Each %XX sequence is + * replaced with the corresponding byte value. + * + * @param str + * The percent-encoded string to decode, or NULL. + * + * @return + * A newly allocated decoded string, or NULL if the input was NULL. + * The caller must free the returned string with guac_mem_free(). + */ +char* guac_rdp_percent_decode(const char* str); + +/** + * Retrieves an Azure AD access token using the OAuth2 Authorization Code + * flow. This function automates the browser-based login by fetching the + * Microsoft login page, extracting session tokens, posting credentials, + * and exchanging the resulting authorization code for an access token. + * + * @param client + * The guac_client associated with the RDP connection. + * + * @param params + * The AAD authentication parameters including tenant ID, client ID, + * username, password, scope, and optional req_cnf. + * + * @return + * A newly allocated string containing the access token, or NULL if + * authentication failed. The caller must free the returned string. + */ +char* guac_rdp_aad_get_token_authcode(guac_client* client, + guac_rdp_aad_params* params); + +#endif /* GUAC_RDP_AAD_H */ diff --git a/src/protocols/rdp/beep.c b/src/protocols/rdp/beep.c index 06b786fdfc..bc0df7156c 100644 --- a/src/protocols/rdp/beep.c +++ b/src/protocols/rdp/beep.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "beep.h" #include "rdp.h" #include "settings.h" diff --git a/src/protocols/rdp/channels/audio-input/audio-buffer.c b/src/protocols/rdp/channels/audio-input/audio-buffer.c index 38d5b7f1e7..b46bc90f13 100644 --- a/src/protocols/rdp/channels/audio-input/audio-buffer.c +++ b/src/protocols/rdp/channels/audio-input/audio-buffer.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/audio-input/audio-buffer.h" #include "rdp.h" diff --git a/src/protocols/rdp/channels/audio-input/audio-input.c b/src/protocols/rdp/channels/audio-input/audio-input.c index 12a9ccb716..e9658cdc0e 100644 --- a/src/protocols/rdp/channels/audio-input/audio-input.c +++ b/src/protocols/rdp/channels/audio-input/audio-input.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/audio-input/audio-buffer.h" #include "channels/audio-input/audio-input.h" #include "plugins/channels.h" diff --git a/src/protocols/rdp/channels/common-svc.c b/src/protocols/rdp/channels/common-svc.c index 774316f4f2..e3a25ea5eb 100644 --- a/src/protocols/rdp/channels/common-svc.c +++ b/src/protocols/rdp/channels/common-svc.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/common-svc.h" #include "plugins/channels.h" #include "rdp.h" diff --git a/src/protocols/rdp/channels/disp.c b/src/protocols/rdp/channels/disp.c index da1ca800d0..f0463b95ba 100644 --- a/src/protocols/rdp/channels/disp.c +++ b/src/protocols/rdp/channels/disp.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/disp.h" #include "plugins/channels.h" #include "fs.h" diff --git a/src/protocols/rdp/channels/pipe-svc.c b/src/protocols/rdp/channels/pipe-svc.c index 68d43488eb..20860a755a 100644 --- a/src/protocols/rdp/channels/pipe-svc.c +++ b/src/protocols/rdp/channels/pipe-svc.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/common-svc.h" #include "channels/pipe-svc.h" #include "common/list.h" diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-fs.c b/src/protocols/rdp/channels/rdpdr/rdpdr-fs.c index ed67ac6ff5..9db2f7ecb2 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr-fs.c +++ b/src/protocols/rdp/channels/rdpdr/rdpdr-fs.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpdr/rdpdr-fs.h" #include "channels/rdpdr/rdpdr-fs-messages.h" #include "channels/rdpdr/rdpdr.h" diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c index 5fee4658bc..8d23efbc18 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c +++ b/src/protocols/rdp/channels/rdpdr/rdpdr-messages.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpdr/rdpdr-messages.h" #include "channels/rdpdr/rdpdr.h" #include "rdp.h" diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c index 4ff705355e..b7d5f022b2 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c +++ b/src/protocols/rdp/channels/rdpdr/rdpdr-printer.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpdr/rdpdr-printer.h" #include "channels/rdpdr/rdpdr.h" #include "print-job.h" diff --git a/src/protocols/rdp/channels/rdpdr/rdpdr.c b/src/protocols/rdp/channels/rdpdr/rdpdr.c index eaeed77465..a3922fe1cd 100644 --- a/src/protocols/rdp/channels/rdpdr/rdpdr.c +++ b/src/protocols/rdp/channels/rdpdr/rdpdr.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpdr/rdpdr.h" #include "channels/rdpdr/rdpdr-fs.h" #include "channels/rdpdr/rdpdr-messages.h" diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c index a94faa9f49..e9a9f532e9 100644 --- a/src/protocols/rdp/channels/rdpei.c +++ b/src/protocols/rdp/channels/rdpei.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpei.h" #include "plugins/channels.h" #include "rdp.h" diff --git a/src/protocols/rdp/channels/rdpgfx.c b/src/protocols/rdp/channels/rdpgfx.c index 0fae972f63..17babd6d6b 100644 --- a/src/protocols/rdp/channels/rdpgfx.c +++ b/src/protocols/rdp/channels/rdpgfx.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpgfx.h" #include "plugins/channels.h" #include "rdp.h" diff --git a/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c index 7c57bc5c58..d438b96c9b 100644 --- a/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c +++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/rdpsnd/rdpsnd-messages.h" #include "channels/rdpsnd/rdpsnd.h" #include "rdp.h" diff --git a/src/protocols/rdp/channels/rdpsnd/rdpsnd.c b/src/protocols/rdp/channels/rdpsnd/rdpsnd.c index bbbe6b5ef1..20c2ee0ecb 100644 --- a/src/protocols/rdp/channels/rdpsnd/rdpsnd.c +++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/common-svc.h" #include "channels/rdpsnd/rdpsnd.h" #include "channels/rdpsnd/rdpsnd-messages.h" diff --git a/src/protocols/rdp/download.c b/src/protocols/rdp/download.c index 1b6eeb76ba..8c5081d269 100644 --- a/src/protocols/rdp/download.c +++ b/src/protocols/rdp/download.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "common/json.h" #include "download.h" #include "fs.h" diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c index 68fe36e785..45ac295602 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "color.h" #include "rdp.h" #include "settings.h" diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index 34f9e7c4af..63c0be2d42 100644 --- a/src/protocols/rdp/input.c +++ b/src/protocols/rdp/input.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/disp.h" #include "channels/rdpei.h" #include "input.h" diff --git a/src/protocols/rdp/plugins/channels.c b/src/protocols/rdp/plugins/channels.c index 3ae3a85517..d2d23d9519 100644 --- a/src/protocols/rdp/plugins/channels.c +++ b/src/protocols/rdp/plugins/channels.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "plugins/channels.h" #include "rdp.h" diff --git a/src/protocols/rdp/plugins/guacai/guacai-messages.c b/src/protocols/rdp/plugins/guacai/guacai-messages.c index 9d04a2388d..43d3ccf3a7 100644 --- a/src/protocols/rdp/plugins/guacai/guacai-messages.c +++ b/src/protocols/rdp/plugins/guacai/guacai-messages.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "channels/audio-input/audio-buffer.h" #include "plugins/guacai/guacai-messages.h" #include "rdp.h" diff --git a/src/protocols/rdp/pointer.c b/src/protocols/rdp/pointer.c index 7387b46b8f..a8ff22341d 100644 --- a/src/protocols/rdp/pointer.c +++ b/src/protocols/rdp/pointer.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "color.h" #include "gdi.h" #include "pointer.h" diff --git a/src/protocols/rdp/print-job.c b/src/protocols/rdp/print-job.c index 2b6ca1aa3d..039c107570 100644 --- a/src/protocols/rdp/print-job.c +++ b/src/protocols/rdp/print-job.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "print-job.h" #include "rdp.h" diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index d0e5a187be..715c2e71d0 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -33,7 +33,6 @@ #include "channels/rdpsnd/rdpsnd.h" #include "client.h" #include "color.h" -#include "config.h" #include "error.h" #include "fs.h" #include "gdi.h" @@ -53,6 +52,7 @@ #include #include +#include #include #include #include @@ -76,6 +76,7 @@ #include #include +#include #include #include @@ -327,6 +328,149 @@ static BOOL rdp_freerdp_authenticate(freerdp* instance, char** username, } +#ifdef HAVE_FREERDP_AAD_SUPPORT +#include "aad.h" + +/** + * Callback invoked by FreeRDP when an Azure AD access token is required. + * + * @param instance + * The FreeRDP instance associated with the RDP session. + * + * @param tokenType + * The type of access token being requested (ACCESS_TOKEN_TYPE_AAD or + * ACCESS_TOKEN_TYPE_AVD). + * + * @param token + * Pointer to a string which will receive the access token. This function + * must allocate and populate this string. + * + * @param count + * Number of additional variadic arguments. + * + * @param ... + * Additional arguments (for AAD: scope and req_cnf) + * + * @return + * TRUE if an access token was successfully obtained, FALSE otherwise. + */ +static BOOL rdp_freerdp_get_access_token(freerdp* instance, + AccessTokenType tokenType, char** token, size_t count, ...) { + + rdpContext* context = GUAC_RDP_CONTEXT(instance); + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_settings* settings = rdp_client->settings; + + *token = NULL; + + /* Only handle ACCESS_TOKEN_TYPE_AAD for now */ + if (tokenType != ACCESS_TOKEN_TYPE_AAD) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Unsupported token type: %d", tokenType); + return FALSE; + } + + /* Extract scope and req_cnf from variadic arguments */ + if (count < 2) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Expected 2 arguments (scope and req_cnf)"); + return FALSE; + } + + va_list ap; + va_start(ap, count); + const char* scope = va_arg(ap, const char*); + const char* req_cnf = va_arg(ap, const char*); + va_end(ap); + + /* Prompt for missing credentials if the client supports it */ + if (settings->username == NULL || settings->password == NULL) { + + if (!guac_client_owner_supports_required(client)) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Username and password are required for AAD " + "authentication, and client does not support prompting"); + return FALSE; + } + + char* required_params[3] = {NULL}; + int i = 0; + + if (settings->username == NULL) { + guac_argv_register(GUAC_RDP_ARGV_USERNAME, + guac_rdp_argv_callback, NULL, 0); + required_params[i++] = GUAC_RDP_ARGV_USERNAME; + } + + if (settings->password == NULL) { + guac_argv_register(GUAC_RDP_ARGV_PASSWORD, + guac_rdp_argv_callback, NULL, 0); + required_params[i++] = GUAC_RDP_ARGV_PASSWORD; + } + + required_params[i] = NULL; + + guac_client_owner_send_required(client, + (const char**) required_params); + guac_argv_await((const char**) required_params); + + /* Check that credentials were provided */ + if (settings->username == NULL || settings->password == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Username and password are required for AAD " + "authentication"); + return FALSE; + } + } + + /* Read client ID from FreeRDP settings (GatewayAvdClientID), which + * defaults to Microsoft's well-known AVD client ID */ + const char* client_id = freerdp_settings_get_string( + context->settings, FreeRDP_GatewayAvdClientID); + if (client_id == NULL || strlen(client_id) == 0) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: No client ID available from FreeRDP settings"); + return FALSE; + } + + /* Use "common" tenant for multi-tenant authentication */ + const char* tenant_id = GUAC_AAD_DEFAULT_TENANT_ID; + + /* Decode the FreeRDP scope, which arrives pre-URL-encoded (e.g., %3A + * instead of :). We must decode it to avoid double-encoding. */ + char* decoded_scope = guac_rdp_percent_decode(scope); + + /* Prepare AAD parameters */ + guac_rdp_aad_params params = { + .tenant_id = (char*) tenant_id, + .client_id = (char*) client_id, + .username = settings->username, + .password = settings->password, + .scope = decoded_scope, + .req_cnf = (char*) req_cnf + }; + + /* Retrieve access token via automated auth code flow */ + char* access_token = guac_rdp_aad_get_token_authcode(client, ¶ms); + + if (access_token == NULL) { + guac_client_log(client, GUAC_LOG_ERROR, + "AAD: Failed to obtain access token"); + guac_mem_free(decoded_scope); + return FALSE; + } + + *token = access_token; + guac_mem_free(decoded_scope); + + guac_client_log(client, GUAC_LOG_INFO, + "AAD: Obtained access token"); + + return TRUE; +} +#endif + #ifdef HAVE_FREERDP_VERIFYCERTIFICATEEX /** * Callback invoked by FreeRDP when the SSL/TLS certificate of the RDP server @@ -553,6 +697,10 @@ static int guac_rdp_handle_connection(guac_client* client) { rdp_inst->PreConnect = rdp_freerdp_pre_connect; rdp_inst->Authenticate = rdp_freerdp_authenticate; +#ifdef HAVE_FREERDP_AAD_SUPPORT + rdp_inst->GetAccessToken = rdp_freerdp_get_access_token; +#endif + #ifdef HAVE_FREERDP_VERIFYCERTIFICATEEX rdp_inst->VerifyCertificateEx = rdp_freerdp_verify_certificate; #else diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index 7490d4d0b4..296dedb7bc 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -820,6 +820,12 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->security_mode = GUAC_SECURITY_ANY; } + /* Azure AD authentication */ + else if (strcmp(argv[IDX_SECURITY], "aad") == 0) { + guac_user_log(user, GUAC_LOG_INFO, "Security mode: Azure AD"); + settings->security_mode = GUAC_SECURITY_AAD; + } + /* If nothing given, default to RDP */ else { guac_user_log(user, GUAC_LOG_INFO, "No security mode specified. Defaulting to security mode negotiation with server."); @@ -1356,7 +1362,7 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->wol_wait_time = guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_WOL_WAIT_TIME, GUAC_WOL_DEFAULT_BOOT_WAIT_TIME); - + } /* Success */ @@ -1620,6 +1626,9 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, TRUE); freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + #ifdef HAVE_FREERDP_AAD_SUPPORT + freerdp_settings_set_bool(rdp_settings, FreeRDP_AadSecurity, FALSE); + #endif break; /* Network level authentication */ @@ -1628,6 +1637,9 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, FALSE); freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, TRUE); freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + #ifdef HAVE_FREERDP_AAD_SUPPORT + freerdp_settings_set_bool(rdp_settings, FreeRDP_AadSecurity, FALSE); + #endif break; /* Extended network level authentication */ @@ -1636,6 +1648,9 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, FALSE); freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, TRUE); + #ifdef HAVE_FREERDP_AAD_SUPPORT + freerdp_settings_set_bool(rdp_settings, FreeRDP_AadSecurity, FALSE); + #endif break; /* Hyper-V "VMConnect" negotiation mode */ @@ -1645,8 +1660,30 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, TRUE); freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); freerdp_settings_set_bool(rdp_settings, FreeRDP_VmConnectMode, TRUE); + #ifdef HAVE_FREERDP_AAD_SUPPORT + freerdp_settings_set_bool(rdp_settings, FreeRDP_AadSecurity, FALSE); + #endif break; + /* Azure AD authentication */ + case GUAC_SECURITY_AAD: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + + /* Enable AAD authentication in FreeRDP (only available in FreeRDP 3.0+) */ + #ifdef HAVE_FREERDP_AAD_SUPPORT + freerdp_settings_set_bool(rdp_settings, FreeRDP_AadSecurity, TRUE); + guac_client_log(client, GUAC_LOG_INFO, + "Azure AD authentication enabled"); + #else + guac_client_log(client, GUAC_LOG_ERROR, + "Azure AD authentication requested but not supported by this version of FreeRDP. " + "FreeRDP 3.0 or later is required."); + #endif + break; + /* All security types */ case GUAC_SECURITY_ANY: freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, TRUE); @@ -1667,6 +1704,10 @@ void guac_rdp_push_settings(guac_client* client, freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, TRUE); freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + + #ifdef HAVE_FREERDP_AAD_SUPPORT + freerdp_settings_set_bool(rdp_settings, FreeRDP_AadSecurity, FALSE); + #endif break; } @@ -1885,6 +1926,13 @@ void guac_rdp_push_settings(guac_client* client, rdp_settings->VmConnectMode = TRUE; break; + /* Azure AD authentication (not supported on FreeRDP 2) */ + case GUAC_SECURITY_AAD: + guac_client_log(client, GUAC_LOG_ERROR, + "Azure AD authentication is not supported by this " + "version of FreeRDP. FreeRDP 3.0 or later is required."); + break; + /* All security types */ case GUAC_SECURITY_ANY: rdp_settings->RdpSecurity = TRUE; diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h index f23cefbe3c..0896e0de65 100644 --- a/src/protocols/rdp/settings.h +++ b/src/protocols/rdp/settings.h @@ -117,6 +117,11 @@ typedef enum guac_rdp_security { */ GUAC_SECURITY_VMCONNECT, + /** + * Azure Active Directory authentication. + */ + GUAC_SECURITY_AAD, + /** * Negotiate a security method supported by both server and client. */ @@ -766,4 +771,3 @@ int guac_rdp_get_height(freerdp* rdp); int guac_rdp_get_depth(freerdp* rdp); #endif - diff --git a/src/protocols/rdp/upload.c b/src/protocols/rdp/upload.c index ecc497180b..1e783b70c1 100644 --- a/src/protocols/rdp/upload.c +++ b/src/protocols/rdp/upload.c @@ -17,6 +17,8 @@ * under the License. */ +#include "config.h" + #include "fs.h" #include "rdp.h" #include "upload.h"