Skip to content

GUACAMOLE-2210: Add support for AAD authentication to RDP protocol. - #633

Open
aleitner wants to merge 1 commit into
apache:mainfrom
aleitner:GUACAMOLE-2210-AAD
Open

aleitner wants to merge 1 commit into
apache:mainfrom
aleitner:GUACAMOLE-2210-AAD

Conversation

@aleitner

@aleitner aleitner commented Feb 13, 2026

Copy link
Copy Markdown
Contributor
  • Adds Azure AD (Entra ID) authentication for RDP connections using FreeRDP 3's AadSecurity mode
  • Uses the OAuth2 device authorization grant (RFC 8628), so no credentials are stored in or entered through Guacamole. The user completes sign-in on a separate device (typically a phone) by scanning a QR code
  • HTTP requests use libcurl; token/device-code responses are parsed with WinPR's JSON API

Authentication flow

When security=aad is set on an RDP connection:

  1. FreeRDP begins the AAD handshake and invokes the GetAccessToken callback with the requested scope and a Proof-of-Possession key confirmation (req_cnf).
  2. guacd requests a device code from Azure AD using the configured application (client) ID and scope, receiving a user code and a verification URI.
  3. guacd opens a pipe stream to the connection owner carrying the user code and verification URI as JSON. The web client renders this as a QR code (paired change: apache/guacamole-client#1168).
  4. guacd polls the token endpoint (grant_type=urn:ietf:params:oauth:grant-type:device_code) while the user authenticates on their own device, forwarding req_cnf so Azure issues a Proof-of-Possession token. Polling honors the server-provided interval and slow_down, and stops if the connection closes or the code expires.
  5. When the flow completes, the pipe stream is closed (dismissing the prompt) and the access token is handed to FreeRDP, which finishes the AAD-authenticated RDP session.

Configuration

New RDP parameters:

  • aad-client-id (required for AAD) is the Azure AD application (client) ID to authenticate as. The app must have public client flows enabled so it can use the device code grant.
  • aad-scope (optional) is the OAuth2 scope to request. If omitted, the scope FreeRDP derives for the target host is used.
  • aad-tenant-id (optional) is the Azure AD tenant, defaulting to organizations.

Notes

  • The entire feature is gated on HAVE_FREERDP_AAD_SUPPORT (FreeRDP built with AAD) and the presence of libcurl. If either is unavailable it compiles out cleanly, and non-AAD RDP is unaffected.
  • Adds a unit test for the percent-decoding helper.

Paired with apache/guacamole-client#1168

@aleitner

Copy link
Copy Markdown
Contributor Author

Didn't realize we don't have curl. This would require adding curl as a dependency

@necouchman necouchman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the build is failing with an error about the switch() statement and one of the enums.

Comment thread src/protocols/rdp/rdp.c Outdated
@aleitner

Copy link
Copy Markdown
Contributor Author

Looks like the build is failing with an error about the switch() statement and one of the enums.

Ahh whoops! I was testing with only freerdp3 as that's when AAD support was implemented. Just added a case to the switch for freerdp2 so that it errors if AAD is selected

Comment thread src/protocols/rdp/aad.c Outdated
@aleitner
aleitner force-pushed the GUACAMOLE-2210-AAD branch from 7989a72 to 37b1d5a Compare April 1, 2026 21:13

@necouchman necouchman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments/changes. Also, generally:

  • It seems like there might be a couple of opportunities for reusing code by creating a couple of new functions?
  • Some of the documentation within the code is a bit sparse and could use a little more building out.

Comment thread src/protocols/rdp/settings.c Outdated
Comment thread src/protocols/rdp/settings.c Outdated
Comment thread src/protocols/rdp/rdp.c Outdated
Comment thread src/protocols/rdp/rdp.c
Comment thread src/protocols/rdp/rdp.c Outdated
Comment thread src/protocols/rdp/rdp.c
Comment thread src/protocols/rdp/rdp.c Outdated
Comment thread src/protocols/rdp/aad.c
Comment thread src/protocols/rdp/aad.c Outdated
Comment thread src/protocols/rdp/aad.c Outdated
@aleitner

aleitner commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Also fixed two runtime issues from changes Microsoft made to the AAD login flow. The credential POST field name needs to be flowToken (camelCase) per $Config.sFTName, and for AVD sessions Microsoft now serves a /common/rdp/set device-binding page that the browser auto-submits before the OAuth redirect, which we replicate with a second POST

@mike-jumper
mike-jumper changed the base branch from staging/1.6.1 to main July 14, 2026 03:51
@aleitner
aleitner force-pushed the GUACAMOLE-2210-AAD branch 2 times, most recently from 685437e to eb58713 Compare July 21, 2026 17:16
@aleitner

Copy link
Copy Markdown
Contributor Author

This has been reworked from the original login-page automation to the OAuth2 device authorization grant (RFC 8628), so the user signs in on a separate device via a QR code and no credentials pass through Guacamole.

Comment thread src/protocols/rdp/Makefile.am
Comment thread src/protocols/rdp/Makefile.am Outdated
Comment thread src/protocols/rdp/aad.c Outdated
Comment thread src/protocols/rdp/aad.c
Comment thread src/protocols/rdp/aad.c
Comment thread src/protocols/rdp/aad.c Outdated
Comment thread src/protocols/rdp/aad.c Outdated
@aleitner
aleitner force-pushed the GUACAMOLE-2210-AAD branch from eb58713 to 954e72d Compare August 20, 2026 14:17

@bbennett-ks bbennett-ks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@aleitner
aleitner force-pushed the GUACAMOLE-2210-AAD branch from 954e72d to b91cf82 Compare August 20, 2026 16:29
Comment thread src/protocols/rdp/aad.c Outdated
* not held open. */
guac_stream* stream = guac_client_alloc_stream(client);
if (stream == NULL) {
guac_client_log(client, GUAC_LOG_DEBUG,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a warning?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a warning.

Comment thread src/protocols/rdp/aad.c
guac_client_log(client, GUAC_LOG_DEBUG,
"AAD: Failed to send the sign-in prompt");

guac_client_free_stream(client, stream);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If guac_protocol_send_pipe() succeeds, but guac_protocol_send_blob() fails, send & flush?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked so end and flush always run once the pipe is open, even if the blob send fails.

Comment thread src/protocols/rdp/aad.c
/* Store the prompt so users who join or reconnect while sign-in is pending
* are brought up to date, then show it to the current owner */
char* body = guac_rdp_aad_build_prompt_json(dc);
if (body != NULL) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log a warning if body == NULL?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a warning on the null path.

Comment thread src/protocols/rdp/aad.c
/* Sleep in short slices so a disconnect is noticed within ~1s rather
* than after the full polling interval */
for (int i = 0; i < interval && client->state == GUAC_CLIENT_RUNNING; i++)
sleep(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In for (int i loop, maybe check for time(NULL) < deadline and guac_client_for_owner(client, guac_rdp_aad_owner_present, NULL), like in the while loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the deadline and owner checks to the sleep loop too.

@aleitner
aleitner force-pushed the GUACAMOLE-2210-AAD branch from b91cf82 to b34a98d Compare August 20, 2026 20:00

@bbennett-ks bbennett-ks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New changes LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants