microsoft / microsoft/adaptive-apps
Backend cannot connect to Azure Event Grid MQTT: the Entra token is sent as a CONNECT password instead of MQTT v5 enhanced authentication
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 4
- Forks
- 3
- PR merge metrics
- No merged PRs in 30d
Description
Background, for readers who don't live in MQTT
If you're already fluent in MQTT and Event Grid, skip to Summary. Otherwise this section explains just enough to make the bug obvious.
How an MQTT client proves who it is. An MQTT client opens a session by sending a CONNECT packet. Since MQTT 3.1, that packet has carried two optional fields called Username and Password — the classic way brokers authenticate clients. Eclipse Mosquitto, the broker this repo uses locally, works exactly like that.
MQTT 5 added a second, different mechanism. MQTT 5 introduced enhanced authentication: two extra CONNECT properties named Authentication Method and Authentication Data, plus a dedicated AUTH packet for multi-step or renewed authentication. This exists precisely so that token-based schemes (OAuth, JWT) have a proper home instead of being smuggled through a field designed for passwords.
Azure Event Grid only accepts the second one. Event Grid's MQTT broker supports Microsoft Entra JWT authentication exclusively through enhanced authentication. The client must set:
Authentication Method=OAUTH2-JWTAuthentication Data= the raw Entra access token, withaud=https://eventgrid.azure.net/
This requires MQTT v5. Event Grid never reads the CONNECT Password field looking for a token. There is no fallback path.
Why that matters here. The backend in this repo puts the token in the Password field. Every connection to Event Grid is therefore refused, and no amount of Azure-side configuration changes that.
Summary
src/backend/Program.cs authenticates to the MQTT broker with WithCredentials(username, token) — the MQTT username/password path. When the broker is Azure Event Grid, the connection is always rejected, because Event Grid requires the token in the MQTT v5 enhanced-authentication fields.
The consequence is not a degraded feature. It takes down the entire backend and fails the whole Radius deployment.
Impact
MqttOrderListener is a BackgroundService. When ConnectAsync leaves the client unconnected, the next line calls SubscribeAsync, which throws MqttClientNotConnectedException. .NET's default BackgroundServiceExceptionBehavior.StopHost then stops the host, so the container exits and enters CrashLoopBackOff:
"code": "Internal",
"message": "Container state is 'Waiting' Reason: CrashLoopBackOff,
back-off 5m0s restarting failed container=backend ..."
Because Radius waits for the container to become Ready, rad deploy fails before the frontend container is created. On an AKS environment that registers the mqtt-azure-event-grid recipe, the application is not merely missing live order streaming — it cannot be deployed at all.
To be explicit about what does not fix it: provisioning a user-assigned managed identity, federated credentials, topic spaces, permission bindings and the EventGrid TopicSpaces Publisher/Subscriber role assignments is all correct and necessary work, and the broker will still refuse every connection. The defect is in the client.
The code
optionsBuilder = optionsBuilder
.WithTlsOptions(tls => tls.UseTls())
// MQTTnet v5 uses username/password credentials for broker auth.
// Event Grid validates the JWT bearer token provided as password.
.WithCredentials(
string.IsNullOrWhiteSpace(azureClientId) ? "oauth2-jwt" : azureClientId,
accessToken.Token);
_logger.LogInformation("MQTT: using OAUTH2-JWT authentication (audience: {Audience})", tokenAudience);
The comment on lines 200–201 states the incorrect assumption directly. WithAuthentication does not appear anywhere in the file — the enhanced-authentication fields are never populated. Everything else in the block is right: TLS is enabled, the audience defaults correctly, and the credential selection logic is sound.
Why this is easy to miss
The failure mode actively disguises itself:
AZURE_CLIENT_IDis empty in the shippedradius/app.bicep, so the code falls through toDefaultAzureCredential, which reaches IMDS and successfully obtains the node identity's token.- Token acquisition therefore succeeds, so the
catch (CredentialUnavailableException or AuthenticationFailedException)on line 187 never fires. - Line 206 then logs
MQTT: using OAUTH2-JWT authentication— which reads like a success message. - Only afterwards does Event Grid reject the CONNECT, and the visible error is
MqttClientNotConnectedExceptionfromSubscribeAsync.
The logs say authentication is working, and the exception points at subscription. Nothing points at the CONNECT packet.
Reproduction
- Deploy a Radius environment that registers
recipes/mqtt-azure-event-grid:latestforRadius.Resources/mqttBrokers(this is whatradius/aks-env.bicepdoes). rad deploy radius/app.bicepagainst that environment.trading-mqttprovisions successfully; thebackendcontainer crash-loops and the deployment fails.kubectl logs deploy/backendshowsMQTT: using OAUTH2-JWT authenticationfollowed byMQTTnet.Exceptions.MqttClientNotConnectedException.
Switching the same environment to recipes/mqtt:latest (Mosquitto) makes the backend healthy immediately, because that recipe returns authMethod: 'none' and the token branch is skipped entirely. That's a workaround, not a fix — it gives up the managed broker.
Suggested fix
MQTTnet 5.1.0.1559 (already referenced in TradingBackend.csproj) supports this directly:
optionsBuilder = optionsBuilder
.WithTlsOptions(tls => tls.UseTls())
.WithProtocolVersion(MqttProtocolVersion.V500)
// Event Grid reads the Entra token from the MQTT v5 enhanced-authentication
// fields. It never inspects the CONNECT password field.
.WithAuthentication("OAUTH2-JWT", Encoding.UTF8.GetBytes(accessToken.Token));
Two details worth getting right in the same change:
- Client ID must be the Entra object ID. For Entra-authenticated clients, Event Grid expects the CONNECT client identifier to match the authenticating principal's object ID. The current
$"backend-{Guid.NewGuid():N}"on line 148 will not satisfy that. (This is likely the same underlying area as #35, though that issue reports a different symptom.) - Tokens expire. Event Grid disconnects a client when its token expires unless the client re-authenticates by sending an
AUTHpacket with reason code 25 and a fresh token. Without this, the backend will drop off the broker roughly hourly. Worth handling in the same pass.
Related robustness problems
These are separate from the main bug but were exposed by it, and each is cheap to fix:
-
ConnectAsyncis unguarded. Line 234 is not wrapped in atry, and the onlycatchin the method covers token acquisition. Any broker unavailability — a restart, a transient network fault — therefore terminates the whole backend process rather than retrying. Consider either catching and retrying with backoff, or settingBackgroundServiceExceptionBehavior.Ignoreif a degraded backend is preferable to a dead one. A REST API that serves accounts, orders and trades probably shouldn't be killed by a message broker being briefly unreachable. -
radius/app.bicepreads the MQTT auth method from the wrong resource. It wiresMQTT_AUTH_METHODandMQTT_TOKEN_AUDIENCEfrom the workload identity:MQTT_AUTH_METHOD: { value: backendIdentity.properties.authMethod }and
radius/recipes/workload-identity/azure-workload-identity.bicephard-codesauthMethod: 'OAUTH2-JWT'. So on any Azure environment the application attempts token authentication regardless of whichmqttBrokersrecipe the environment registered — the identity silently overrides the platform operator's choice of broker. Registering the Mosquitto recipe on AKS does not work until this is corrected.The
mqttBrokersresource type already exposes both values, andradius/resource-types/types.yamldocuments them as the intended source (mqtt.properties.authMethod,mqtt.properties.tokenAudience). Reading them from the broker is a one-line change per container, is identical in every environment, and lets each recipe answer for itself:MQTT_AUTH_METHOD: { value: tradingMqtt.properties.authMethod } MQTT_TOKEN_AUDIENCE: { value: tradingMqtt.properties.tokenAudience } -
AZURE_TENANT_IDis wired to an empty parameter.app.biceppasses theworkloadIdentityTenantIdparameter straight through, and it defaults to'', so containers receive an empty tenant even though the workload-identity recipe returns the correct one viatenantId. This also prevents theWorkloadIdentityCredentialbranch on line 153 from ever being selected, since it requires a non-empty tenant. Deriving it from the identity resource (with the parameter as an override) fixes both. -
The frontend has no Event Grid path at all.
src/frontend/public/index.htmlconnects with the browsermqtt.jsclient toMQTT_WS_URLwith no credentials. Against Event Grid that cannot work — it needs a token and a browser-reachable endpoint. This may be intentional scope, but it's worth stating in the docs so operators know that selecting the Event Grid recipe does not give them browser-side streaming either. -
The Event Grid recipe returns
resources: [].radius/recipes/mqtt/azure-event-grid.bicepdeliberately reports no resources (the comment cites Radius Azure scope validation). A side effect is that Radius does not track the namespace it created, so switching recipes or runningrad app deleteorphans the Event Grid namespace and it keeps billing. A note in the recipe header would save people money.
Environment
- Repo at
7a200b9296ed23ff847e62ab961f18c3f500b793 - Radius on AKS (Kubernetes 1.31),
Radius.Resources/*types fromradius/resource-types/types.yaml mqttBrokersbound toghcr.io/microsoft/adaptive-apps/recipes/mqtt-azure-event-grid:latest- Event Grid namespace with MQTT enabled, reached over TLS on 8883
- MQTTnet 5.1.0.1559, Azure.Identity 1.13.2, .NET 8
Happy to open a PR for the WithAuthentication change and the app.bicep wiring if that's useful — I've already validated the recipe-selection and wiring fixes end to end on AKS.
Note on #35
This is distinct from #35, which reports trading-mqtt failing with an empty subscription identifier during provisioning. Here the resource provisions cleanly and the failure is at the application's connect time. The two may share a root cause around Entra client identity, but they reproduce independently.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/backend/Program.cs around the MQTT client setup and read TradingBackend.csproj to confirm the MQTTnet version and supported authentication APIs. Then inspect radius/app.bicep, radius/resource-types/types.yaml, and radius/recipes/mqtt/azure-event-grid.bicep for broker-property wiring. Done means Event Grid accepts the backend connection and token lifecycle, broker selection supplies the intended settings, and the documented deployment path no longer crash-loops.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- azure, csharp
- Domain
- authentication, backend, cloud, networking
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100