0

https://learn.microsoft.com/en-us/azure/azure-signalr/signalr-tutorial-build-blazor-server-chat-app

How do I get this to work with Azure AD activated? It works perfect when I run in locally in visual studio, but when deployed it will not work with Azure AD, only if I remove Azure AD it works.

This is the error message when deployed and after clicking the button "Chat!" next to username textbox:

"ERROR: Failed to start chat client: Response status code does not indicate success: 403 (Forbidden)."

(I have found other threads like this Blazor Server SignalR Chat works on Local, not on Azure but no solution)

//Program.cs

using BlazorApp6ADChat;
using BlazorApp6ADChat.Data;
using BlazorChat;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();

builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy
    options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddSingleton<WeatherForecastService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.MapHub<BlazorChatSampleHub>(BlazorChatSampleHub.HubUrl);

app.UseAuthentication();
app.UseAuthorization();

app.Run();

//appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "xxx.onmicrosoft.com",
    "TenantId": "xxx",
    "ClientId": "xxx",
    "CallbackPath": "/signin-oidc"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Working locally/VS

Dharman
  • 30,962
  • 25
  • 85
  • 135
W Tech
  • 65
  • 1
  • 6
  • Could you attach the Azure configuration you have in your appsettings.json file (with remove important data like secret etc.) and the config you have in startup.cs / program.cs please ? Maybe there is a typo of something like it. Maybe check teh environment variable you set in the Azure server (debug/development/prog) ;) – Dylan El Bar May 17 '22 at 09:17
  • I have added the config and code, do you see something that is wrong? It works with Azure AD/user logged in when debugging, and Azure AD works with everything else in my app, just not this SignalRChat example from Microsoft. If I remove Azure AD the chat also works when deployed.. – W Tech May 17 '22 at 09:34
  • Feels weird that chat works with Azure AD enabled from localhost but not published to Azure, when published it only works if I turn off Azure AD. – W Tech May 17 '22 at 09:56
  • @W Tech unfortunately I do not find anything wrong, maybe someone will be able to save you, I hope so :/ – Dylan El Bar May 17 '22 at 13:04
  • 1
    I can post the middleware I added to get WASM Signalr to work with Azure AD. services.TryAddEnumerable( ServiceDescriptor.Singleton, ConfigureJwtBearerOptions>()); – Brian Parker May 18 '22 at 01:59
  • @BrianParker is ConfigureJwtBearerOptions a class that you created and what should be in it? – W Tech May 19 '22 at 06:47
  • 1
    Ok ill post my answer in about 90mins in the middle of something atm. Sorry about the suspense. But yes the ConfigureJwtBearerOptions is a class I created. – Brian Parker May 19 '22 at 08:04
  • @BrianParker Thank you very much for your time and help Brian, it worked right away, I dont really understand why it is needed, but this solution you provided works for all who reads this. – W Tech May 19 '22 at 21:11
  • 1
    @WTech Please mark my answer as accepted then. The reason is SignalR does not have to use TCP and the out of the box solution only attaches the token to TCP packets. This middleware just makes sure the token is attached to the Hub requests. – Brian Parker May 20 '22 at 00:04

2 Answers2

1

Not sure if this will help. This is how I wire up a WebAssembly Host (server) with a SignalR Hub.

services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
    ConfigureJwtBearerOptions>());
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
    public void PostConfigure(string name, JwtBearerOptions options)
    {
        var originalOnMessageReceived = options.Events.OnMessageReceived;
        options.Events.OnMessageReceived = async context =>
        {
            await originalOnMessageReceived(context);

            if (string.IsNullOrEmpty(context.Token))
            {
                var accessToken = context.Request.Query["access_token"];
                var requestPath = context.HttpContext.Request.Path;
                var endPoint = $"/chathub";

                if (!string.IsNullOrEmpty(accessToken) &&
                    requestPath.StartsWithSegments(endPoint))
                {
                    context.Token = accessToken;
                }
            }
        };
    }
}
Brian Parker
  • 11,946
  • 2
  • 31
  • 41
0

I posted a solution here: Microsoft Learn

Basically:

  1. Manually getting the cookies in the host.cshtml page
  2. Passing the Cookie collection to the app.razor so I can create a cascading parameter
  3. Retrieving the parameter and manually populating the cookie container when instantiating the SignalR client (you may have seen this code on the web, but without steps 1 and 2 it will not work on Azure only in IIS Express)

Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddAutoMapper(typeof(Program).Assembly);

         builder.Services.AddHttpClient();
        builder.Services.AddHttpContextAccessor();
        builder.Services.AddScoped<HttpContextAccessor>();          

        builder.Services.AddResponseCompression(opts =>
        {
            opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "application/octet-stream" });
        });

        var app = builder.Build();

        app.UseResponseCompression();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.MapControllers();
        app.MapBlazorHub();
  
        app.MapHub<ChatHub>("/chathub");
        app.MapFallbackToPage("/_Host");

        app.Run();
    }
}

_Host.cshtml:

<body>
    @{
        var CookieCollection = HttpContext.Request.Cookies;
        Dictionary<string, string> Cookies = new Dictionary<string, string>();
        foreach (var cookie in CookieCollection)
        {
            Cookies.Add(cookie.Key, cookie.Value);
        }        
    }
    <component type="typeof(App)" render-mode="ServerPrerendered" param-Cookies="Cookies" />

app.razor:

<CascadingValue Name="Cookies" Value="Cookies">
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>
</CascadingValue>
@code {
    [Parameter] public Dictionary<string, string> Cookies { get; set; }
}

Index.razor:

@code {
    #nullable disable
    [CascadingParameter(Name = "Cookies")] public Dictionary<string, string> Cookies { get; set; }    

    System.Security.Claims.ClaimsPrincipal CurrentUser;
    private HubConnection hubConnection;
    private List<string> messages = new List<string>();
    private string userInput;
    private string messageInput;
    private string strError = "";

    protected override async Task OnInitializedAsync()
    {
        try
        {
            var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();

            CurrentUser = authState.User;

            // ** SignalR Chat

            try
            {
                hubConnection = new HubConnectionBuilder()
                 .WithUrl(Navigation.ToAbsoluteUri("/chathub"), options =>
                 {
                     options.UseDefaultCredentials = true;
                     var cookieCount = Cookies.Count();
                     var cookieContainer = new CookieContainer(cookieCount);
                     foreach (var cookie in Cookies)
                         cookieContainer.Add(new Cookie(
                             cookie.Key,
                             WebUtility.UrlEncode(cookie.Value),
                             path: "/",
                             domain: Navigation.ToAbsoluteUri("/").Host));
                     options.Cookies = cookieContainer;

                     foreach (var header in Cookies)
                         options.Headers.Add(header.Key, header.Value);

                     options.HttpMessageHandlerFactory = (input) =>
                     {
                         var clientHandler = new HttpClientHandler
                             {
                                 PreAuthenticate = true,
                                 CookieContainer = cookieContainer,
                                 UseCookies = true,
                                 UseDefaultCredentials = true,
                             };
                         return clientHandler;
                     };
                 })
                 .WithAutomaticReconnect()
                 .Build();

                hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
                {
                    var encodedMsg = $"{user}: {message}";
                    messages.Add(encodedMsg);
                    InvokeAsync(StateHasChanged);
                });

                await hubConnection.StartAsync();
            }
            catch(Exception ex)
            {
                strError = ex.Message;
            }

        }
        catch
        {
            // do nothing if this fails
        }
    }

    // ** SignalR Chat
    private async Task Send()
    {
        if (hubConnection is not null)
        {
            await hubConnection.SendAsync("SendMessage", messageInput);
        }
    }

    public bool IsConnected =>
        hubConnection?.State == HubConnectionState.Connected;

    public async ValueTask DisposeAsync()
    {
        if (hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}

ChatHub.cs:

[Authorize]
public class ChatHub : Hub
{
    public async override Task OnConnectedAsync()
    {
        if (this.Context.User?.Identity?.Name != null)
        {
            await Clients.All.SendAsync(
                "broadcastMessage", 
                "_SYSTEM_", 
                $"{Context.User.Identity.Name} JOINED");
        }
    }
    public async Task SendMessage(string message)
    {
        if (this.Context.User?.Identity?.Name != null)
        {
            await Clients.All.SendAsync(
                "ReceiveMessage", 
                this.Context.User.Identity.Name, 
                message);
        }
    }
}
Michael Washington
  • 2,744
  • 20
  • 29