- Published on
SignalR and Container Apps Easy Auth Log Avalanche
- Authors
- Name
- David Rosevear
TL;DR: Upon SignalR reconnection, the browser sends a /negotiate
request to the Container App and includes an unnecessary Authorization
header whose value is not intended for the Easy Auth sidecar container. This results in the Easy Auth sidecar container producing a huge number of logs, costing you money 💸
Some background
The app that I refer to in this post is powered by an Azure Container App using Easy Auth along with Azure SignalR for messaging clients.
My colleagues Ali and Chris noticed we'd been spending a frightening amount of money on Log Analytics. The tens of millions of logs were coming from a container called http-auth
, a sidecar container deployed when Easy Auth is on. One of the many complaints that http-auth
had looked something like:
stdout F {
"TaskName": "MiddlewareWarning",
"Message": "JWT validation failed: Issuer validation failure; Audience validation failure; Key ID validation failure.",
"Level": 4,
"EventId": 5
}
http-auth
container
The issue: client negotiation and the When making an initial connection, the SignalR client sends an HTTP /negotiate
request to the SignalR hub that's hosted on the Container App, with the AppServiceAuthSession
cookie as expected (for more on Azure Container Apps, Easy Auth and .NET authentication, John Reilly is your guy!). The hub replies, giving the client the SignalR service URL and an access token corresponding to the user's identity. This token is passed in the Authorization
header for subsequent SignalR websocket requests. The access token is meant for the SignalR service and not the Container App.
After this initial connection, if for whatever reason (inactivity, expired token, etc.) the client tries to reconnect (since we use .withAutomaticReconnect
), the client once again sends a /negotiate
request to the Container App, but this time with the SignalR access token included in the Authorization
header. Since this request is headed for the container app, the Easy Auth sidecar notices the Authorization
header and tries to validate its value, causing the noisy and expensive logs. The request makes it through though, since the AppServiceAuthSession
cookie is still present.
Significantly, this SignalR access token's claims don't match those that the Easy Auth sidecar container expects. For example, audience is expected to be the AD app client ID as opposed to the SignalR service URL. This token was never meant to be seen by Easy Auth, anyway.
The solution: don't negotiate with an access token
The fix was to make sure that the client never bothers to add an Authorization header to a /negotiate
request. Instead, we rely just on the AppServiceAuthSession
cookie as usual. All we need in the SignalR client is accessTokenFactory: () => ""
, thanks to Easy Auth. And so our client looks like so:
const newConnection = new signalR.HubConnectionBuilder()
.withUrl('/api/hub', {
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => '', // to prevent the client from sending an access token when negotiating a connection upon reconnect (auth is handled by the cookie + Easy Auth)
})
.configureLogging(signalR.LogLevel.Information)
.withAutomaticReconnect()
.build()
And yay! Things cleared up! 🥳